好库文摘 http://doc.okbase.net/ Java中的异常和处理详解 http://doc.okbase.net/lulipro/archive/265415.html 代码钢琴家 2017/9/13 22:27:59

简介

程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常。异常发生时,是任程序自生自灭,立刻退出终止,还是输出错误给用户?或者用C语言风格:用函数返回值作为执行状态?。
 
Java提供了更加优秀的解决办法:异常处理机制。
 
异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。
Java中的异常可以是函数中的语句执行时引发的,也可以是程序员通过throw 语句手动抛出的,只要在Java程序中产生了异常,就会用一个对应类型的异常对象来封装异常,JRE就会试图寻找异常处理程序来处理异常。
 
Throwable类是Java异常类型的顶层父类,一个对象只有是 Throwable 类的(直接或者间接)实例,他才是一个异常对象,才能被异常处理机制识别。JDK中内建了一些常用的异常类,我们也可以自定义异常。

Java异常的分类和类结构图

Java标准裤内建了一些通用的异常,这些类以Throwable为顶层父类。

Throwable又派生出Error类和Exception类。

错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。

异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。

 

总体上我们根据Javac对异常的处理要求,将异常类分为2类。

非检查异常(unckecked exception):Error 和 RuntimeException 以及他们的子类。javac在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用try...catch...finally)这样的异常,也可以不处理。对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。如除0错误ArithmeticException,错误的强制类型转换错误ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,使用了空对象NullPointerException等等。

 

检查异常(checked exception):除了Error 和 RuntimeException的其它异常。javac强制要求程序员为这样的异常做预备处理工作(使用try...catch...finally或者throws)。在方法中要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如SQLException , IOException,ClassNotFoundException 等。

需要明确的是:检查和非检查是对于javac来说的,这样就很好理解和区分了。

初识异常

下面的代码会演示2个异常类型:ArithmeticException 和 InputMismatchException。前者由于整数除0引发,后者是输入的数据不能被转换为int类型引发。

package com.example;
import java. util .Scanner ;
public class AllDemo
{
      public static void main (String [] args )
      {
            System . out. println( "----欢迎使用命令行除法计算器----" ) ;
            CMDCalculate ();
      }
      public static void CMDCalculate ()
      {
            Scanner scan = new Scanner ( System. in );
            int num1 = scan .nextInt () ;
            int num2 = scan .nextInt () ;
            int result = devide (num1 , num2 ) ;
            System . out. println( "result:" + result) ;
            scan .close () ;
      }
      public static int devide (int num1, int num2 ){
            return num1 / num2 ;
      }
}
/*****************************************

----欢迎使用命令行除法计算器----
2
0
Exception in thread "main" java.lang.ArithmeticException : / by zero
     at com.example.AllDemo.devide( AllDemo.java:30 )
     at com.example.AllDemo.CMDCalculate( AllDemo.java:22 )
     at com.example.AllDemo.main( AllDemo.java:12 )

----欢迎使用命令行除法计算器----
1
r
Exception in thread "main" java.util.InputMismatchException
     at java.util.Scanner.throwFor( Scanner.java:864 )
     at java.util.Scanner.next( Scanner.java:1485 )
     at java.util.Scanner.nextInt( Scanner.java:2117 )
     at java.util.Scanner.nextInt( Scanner.java:2076 )
     at com.example.AllDemo.CMDCalculate( AllDemo.java:20 )
     at com.example.AllDemo.main( AllDemo.java:12 )
*****************************************/

 

异常是在执行某个函数时引发的,而函数又是层级调用,形成调用栈的,因为,只要一个函数发生了异常,那么他的所有的caller都会被异常影响。当这些被影响的函数以异常信息输出时,就形成的了异常追踪栈

异常最先发生的地方,叫做异常抛出点

 

 

从上面的例子可以看出,当devide函数发生除0异常时,devide函数将抛出ArithmeticException异常,因此调用他的CMDCalculate函数也无法正常完成,因此也发送异常,而CMDCalculate的caller——main 因为CMDCalculate抛出异常,也发生了异常,这样一直向调用栈的栈底回溯。这种行为叫做异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的caller中找到最近的异常处理程序。由于这个例子中没有使用任何异常处理机制,因此异常最终由main函数抛给JRE,导致程序终止。

上面的代码不使用异常处理机制,也可以顺利编译,因为2个异常都是非检查异常。但是下面的例子就必须使用异常处理机制,因为异常是检查异常。

代码中我选择使用throws声明异常,让函数的调用者去处理可能发生的异常。但是为什么只throws了IOException呢?因为FileNotFoundException是IOException的子类,在处理范围内。

@Test
public void testException() throws IOException
{
    //FileInputStream的构造函数会抛出FileNotFoundException
    FileInputStream fileIn = new FileInputStream("E:\\a.txt");
    
    int word;
    //read方法会抛出IOException
    while((word =  fileIn.read())!=-1) 
    {
        System.out.print((char)word);
    }
    //close方法会抛出IOException
    fileIn.clos
}

 

异常处理的基本语法

在编写代码处理异常时,对于检查异常,有2种不同的处理方式:使用try...catch...finally语句块处理它。或者,在函数签名中使用throws 声明交给函数调用者caller去解决。
 

try...catch...finally语句块

try{
     //try块中放可能发生异常的代码。
//如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。
//如果发生异常,则尝试去匹配catch块。
}catch(SQLException SQLexception){ //每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。 //catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。 //在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。 //如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。
//如果try中没有发生异常,则所有的catch块将被忽略。
}catch(Exception exception){ //... }finally{
//finally块通常是可选的。
//无论异常是否发生,异常是否匹配被处理,finally都会执行。 //一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。 //finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。 }

 

需要注意的地方

1、try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使用。
 
2、每一个catch块用于处理一个异常。异常匹配是按照catch块的顺序从上往下寻找的,只有第一个匹配的catch会得到执行。匹配时,不仅运行精确匹配,也支持父类匹配,因此,如果同一个try块下的多个catch异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每个catch块都有存在的意义。
 
3、java中,异常处理的任务就是将执行控制流从异常发生的地方转移到能够处理这种异常的地方去。也就是说:当一个函数的某条语句发生异常时,这条语句的后面的语句不会再执行,它失去了焦点。执行流跳转到最近的匹配的异常处理catch代码块去执行,异常被处理完后,执行流会接着在“处理了这个异常的catch代码块”后面接着执行。
有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handling(恢复式异常处理模式 )
而Java则是让执行流恢复到处理了异常的catch块后接着执行,这种策略叫做:termination model of exception handling(终结式异常处理模式)
 
public static void main(String[] args){
        try {
            foo();
        }catch(ArithmeticException ae) {
            System.out.println("处理异常");
        }
}
public static void foo(){
        int a = 5/0;  //异常抛出点
        System.out.println("为什么还不给我涨工资!!!");  //////////////////////不会执行
}

 

throws 函数声明

throws声明:如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉,则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异常,否则编译不通过。

throws是另一种处理异常的方式,它不同于try...catch...finally,throws仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。

采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。

public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN
{ 
     //foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。
}

 

finally块

finally块不管异常是否发生,只要对应的try执行了,则它一定也执行。只有一种方法让finally块不执行:System.exit()。因此finally块通常用来做资源释放操作:关闭文件,关闭数据库连接等等。

良好的编程习惯是:在try块中打开资源,在finally块中清理释放这些资源。

需要注意的地方:

1、finally块没有处理异常的能力。处理异常的只能是catch块。

2、在同一try...catch...finally块中 ,如果try中抛出异常,且有匹配的catch块,则先执行catch块,再执行finally块。如果没有catch块匹配,则先执行finally,然后去外面的调用者中寻找合适的catch块。

3、在同一try...catch...finally块中 ,try发生异常,且匹配的catch块中处理异常时也抛出异常,那么后面的finally也会执行:首先执行finally块,然后去外围调用者中寻找合适的catch块。

这是正常的情况,但是也有特例。关于finally有很多恶心,偏、怪、难的问题,我在本文最后统一介绍了,电梯速达->finally块和return

 

throw 异常抛出语句

throw exceptionObject

程序员也可以通过throw语句手动显式的抛出一个异常。throw语句的后面必须是一个异常对象。

throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,它和由JRE自动形成的异常抛出点没有任何差别。

public void save(User user)
{
      if(user  == null) 
          throw new IllegalArgumentException("User对象为空");
      //......
        
}

 

异常的链化

在一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如骨牌效应一样,将导致一连串的异常。假设B模块完成自己的逻辑需要调用A模块的方法,如果A模块发生异常,则B也将不能完成而发生异常,但是B在抛出异常时,会将A的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。

异常链化:以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带Throwable参数的函数来实现的。这个当做参数的异常,我们叫他根源异常(cause)。

查看Throwable类源码,可以发现里面有一个Throwable字段cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的结点类设计如出一辙,因此形成链也是自然的了。

public class Throwable implements Serializable {
    private Throwable cause = this;
   
    public Throwable(String message, Throwable cause) {
        fillInStackTrace();
        detailMessage = message;
        this.cause = cause;
    }
     public Throwable(Throwable cause) {
        fillInStackTrace();
        detailMessage = (cause==null ? null : cause.toString());
        this.cause = cause;
    }
    
    //........
} 

 

下面是一个例子,演示了异常的链化:从命令行输入2个int,将他们相加,输出。输入的数不是int,则导致getInputNumbers异常,从而导致add函数异常,则可以在add函数中抛出

一个链化的异常。

public static void main(String[] args)
{
    
    System.out.println("请输入2个加数");
    int result;
    try
    {
        result = add();
        System.out.println("结果:"+result);
    } catch (Exception e){
        e.printStackTrace();
    }
}
//获取输入的2个整数返回
private static List<Integer> getInputNumbers()
{
    List<Integer> nums = new ArrayList<>();
    Scanner scan = new Scanner(System.in);
    try {
        int num1 = scan.nextInt();
        int num2 = scan.nextInt();
        nums.add(new Integer(num1));
        nums.add(new Integer(num2));
    }catch(InputMismatchException immExp){
        throw immExp;
    }finally {
        scan.close();
    }
    return nums;
}

//执行加法计算
private static int add() throws Exception
{
    int result;
    try {
        List<Integer> nums =getInputNumbers();
        result = nums.get(0)  + nums.get(1);
    }catch(InputMismatchException immExp){
        throw new Exception("计算失败",immExp);  /////////////////////////////链化:以一个异常对象为参数构造新的异常对象。
    }
    return  result;
}

/*
请输入2个加数
r 1
java.lang.Exception: 计算失败
    at practise.ExceptionTest.add(ExceptionTest.java:53)
    at practise.ExceptionTest.main(ExceptionTest.java:18)
Caused by: java.util.InputMismatchException
    at java.util.Scanner.throwFor(Scanner.java:864)
    at java.util.Scanner.next(Scanner.java:1485)
    at java.util.Scanner.nextInt(Scanner.java:2117)
    at java.util.Scanner.nextInt(Scanner.java:2076)
    at practise.ExceptionTest.getInputNumbers(ExceptionTest.java:30)
    at practise.ExceptionTest.add(ExceptionTest.java:48)
    ... 1 more

*/

 

 

自定义异常

如果要自定义异常类,则扩展Exception类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则扩展自RuntimeException。

按照国际惯例,自定义的异常应该总是包含如下的构造函数:

  • 一个无参构造函数
  • 一个带有String参数的构造函数,并传递给父类的构造函数。
  • 一个带有String参数和Throwable参数,并都传递给父类构造函数
  • 一个带有Throwable 参数的构造函数,并传递给父类的构造函数。
 下面是IOException类的完整源代码,可以借鉴。
public class IOException extends Exception
{
    static final long serialVersionUID = 7818375828146090155L;

    public IOException()
    {
        super();
    }

    public IOException(String message)
    {
        super(message);
    }

    public IOException(String message, Throwable cause)
    {
        super(message, cause);
    }

    
    public IOException(Throwable cause)
    {
        super(cause);
    }
}

 

异常的注意事项

1、当子类重写父类的带有 throws声明的函数时,其throws声明的异常必须在父类异常的可控范围内——用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法 。这是为了支持多态。

例如,父类方法throws 的是2个异常,子类就不能throws 3个及以上的异常。父类throws IOException,子类就必须throws IOException或者IOException的子类。

 至于为什么?我想,也许下面的例子可以说明。

class Father
{
    public void start() throws IOException
    {
        throw new IOException();
    }
}

class Son extends Father
{
    public void start() throws Exception
    {
        throw new SQLException();
    }
}
/**********************假设上面的代码是允许的(实质是错误的)***********************/
class Test
{
    public static void main(String[] args)
    {
        Father[] objs = new Father[2];
        objs[0] = new Father();
        objs[1] = new Son();

        for(Father obj:objs)
        {
        //因为Son类抛出的实质是SQLException,而IOException无法处理它。
        //那么这里的try。。catch就不能处理Son中的异常。
        //多态就不能实现了。
            try {
                 obj.start();
            }catch(IOException)
            {
                 //处理IOException
            }
         }
   }
}
View Code

 

 

2、Java程序可以是多线程的。每一个线程都是一个独立的执行流,独立的函数调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常 会导致程序终止。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。

也就是说,Java中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。

 

finally块和return

首先一个不容易理解的事实:在 try块中即便有return,break,continue等改变执行流的语句,finally也会执行。

public static void main(String[] args)
{
    int re = bar();
    System.out.println(re);
}
private static int bar() 
{
    try{
        return 5;
    } finally{
        System.out.println("finally");
    }
}
/*输出:
finally
5
*/

 很多人面对这个问题时,总是在归纳执行的顺序和规律,不过我觉得还是很难理解。我自己总结了一个方法。用如下GIF图说明。

 也就是说:try...catch...finally中的return 只要能执行,就都执行了,他们共同向同一个内存地址(假设地址是0x80)写入返回值,后执行的将覆盖先执行的数据,而真正被调用者取的返回值就是最后一次写入的。那么,按照这个思想,下面的这个例子也就不难理解了。

finally中的return 会覆盖 try 或者catch中的返回值。

public static void main(String[] args)
    {
        int result;
        
        result  =  foo();
        System.out.println(result);     /////////2
        
        result = bar();
        System.out.println(result);    /////////2
    }

    @SuppressWarnings("finally")
    public static int foo()
    {
        trz{
            int a = 5 / 0;
        } catch (Exception e){
            return 1;
        } finally{
            return 2;
        }

    }

    @SuppressWarnings("finally")
    public static int bar()
    {
        try {
            return 1;
        }finally {
            return 2;
        }
    }

 

finally中的return会抑制(消灭)前面try或者catch块中的异常

class TestException
{
    public static void main(String[] args)
    {
        int result;
        try{
            result = foo();
            System.out.println(result);           //输出100
        } catch (Exception e){
            System.out.println(e.getMessage());    //没有捕获到异常
        }
        
        
        try{
            result  = bar();
            System.out.println(result);           //输出100
        } catch (Exception e){
            System.out.println(e.getMessage());    //没有捕获到异常
        }
    }
    
    //catch中的异常被抑制
    @SuppressWarnings("finally")
    public static int foo() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }catch(ArithmeticException amExp) {
            throw new Exception("我将被忽略,因为下面的finally中使用了return");
        }finally {
            return 100;
        }
    }
    
    //try中的异常被抑制
    @SuppressWarnings("finally")
    public static int bar() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }finally {
            return 100;
        }
    }
}

 

finally中的异常会覆盖(消灭)前面try或者catch中的异常

class TestException
{
    public static void main(String[] args)
    {
        int result;
        try{
            result = foo();
        } catch (Exception e){
            System.out.println(e.getMessage());    //输出:我是finaly中的Exception
        }
        
        
        try{
            result  = bar();
        } catch (Exception e){
            System.out.println(e.getMessage());    //输出:我是finaly中的Exception
        }
    }
    
    //catch中的异常被抑制
    @SuppressWarnings("finally")
    public static int foo() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }catch(ArithmeticException amExp) {
            throw new Exception("我将被忽略,因为下面的finally中抛出了新的异常");
        }finally {
            throw new Exception("我是finaly中的Exception");
        }
    }
    
    //try中的异常被抑制
    @SuppressWarnings("finally")
    public static int bar() throws Exception
    {
        try {
            int a = 5/0;
            return 1;
        }finally {
            throw new Exception("我是finaly中的Exception");
        }
        
    }
}

 

 

上面的3个例子都异于常人的编码思维,因此我建议:

  • 不要在fianlly中使用return。
  • 不要在finally中抛出异常。
  • 减轻finally的任务,不要在finally中做一些其它的事情,finally块仅仅用来释放资源是最合适的。
  • 将尽量将所有的return写在函数的最后面,而不是try ... catch ... finally中。

 

]]>
JavaScript: 使用 atan2 来绘制 箭头 和 曲线 http://doc.okbase.net/f1194361820/archive/265414.html 救火队长 2017/9/13 22:27:53

最近搞Canvas绘图,知道了JavaScript中提供了atan2(y,x)这样一个三角函数。乍眼一看,不认识,毕竟在高中时,学过的三角函数有:sin,cos,arcsin,arccos,tan,arctan等,并没有这个。而工作中又需要用到它,所以这里就做了个简单的了解。

 

  1. 在坐标系中理解tan 和atan
  2. 为何存在atan2 ?
  3. atan2 应用

 

 

 

在坐标系中理解tan atan

回顾一下三角函数tan

tanθ,用三角函数来表示时,它的值等于sinθ/cosθ,如果将其放到坐标系中,它的的值等价于:dy/dx。在坐标系中,任意两个点所组成的直线,相对于x轴的斜率就是tanθ = dy /dx,相对于y轴的斜率就是dx/dy ,此时我们用cot来表示;其中,dy 是两个点的y坐标的差值,dx是两个点的x坐标的差值。

那么坐标系内除了y轴,任何一个点(x,y),相对于x轴的斜率就是y-0/x-0,也即是y/x

 

我们将tanθ称为一条直线相对于x轴的斜率,那么θ就是相对于x轴的夹角(旋转角度)了。

tan,是根据角度计算斜率的。那么反过来 arctan(反正切)自然就认为是根据斜率来计算角度的。

 

为何存在atan2 ?

JavaScript中,提供了两个arctan函数,一个是atan, 一个是atan2atan就是我们所熟知arctan。其实在很多编程语言中都提供了atan2

那么atan2又是怎么回事呢?

 

要知道这个,需要知道arctan的不足之处:

arctan的返回值范围是(-π/2,  π/2) 不包括, ±π/2,也就是(两个点组成的直线与x轴夹角是90°)90°是计算不出来的。为啥呢?在计算arctan ( dy/dx)时,如果两个点(x1,y1),(x2,y2)组成的直线与x轴的夹角呈90°时,dx= x2-x1 = 0 0 是不能作为除数的,所以就无法计算这种情形。

值的范围也就是计算的角度的范围在(-π/2,  π/2),从坐标系来看,这个角度的范围只能是在第14象限,并不能表示出第23象限的角。

 

为了弥补atan的不足,在计算机编程领域,引入了atan2函数,它的计算结果是在(-π,π]。它正好可以覆盖整个坐标系,包括90°的情形。

 

它的计算过程是怎样的呢?

关于这个,我从wikipedia上摘取了它的计算过程:

 

 

 

 

atan2的应用

在第一小节中的那张图中的坐标系,是我们熟知的。在HTMLCanvas中,坐标系并不像我们熟知的坐标系那样。它是这样的:

 

x轴正向沿顺时针方向,所经过的角度分别是0,π/2, π,3π/22π。

x轴正向沿逆时针方向,所经过的角度分别是0-π/2, -π,-3π/2-2π。

 

 

 

atan2的结果在(-π,π]之间,恰好一周,四个象限全覆盖。从坐标系来看,顺时针方向的值是正值,逆时针方向的值是负的。 

从坐标系上来看,atan2结果是(0,-π)时就表示,从x轴正向逆时针方向转最大 π弧度(180角度)。同理,(0,π)表示从x轴正向顺时针转最大π弧度(180角度)。

 

在第1)小节中说了atan可以用来计算平面坐标系内任意两点的连线与x轴正向之间的夹角。而atan2atan的补充,那么使用atan2自然就可以来计算平面坐标系内任意两点的连线与x轴正向之间的夹角了。

 如果两个点在第一象限内:

 

 

如果两个点在第四象限内:

如果两个点在不同的象限内,我们也可以平移来看。

 

 

何时需要使用atan2 ?

 

目前我遇到了两种情况,是通过atan2来解决的:

1) 在平面坐标系内任意两个点间画一条带有箭头的直线(可以是单向箭头,可以是双向箭头)。在这个需求中,另外也知道了箭头的一条边与直线的夹角和箭头的长度。

这个需求的难点就是要计算出箭头的另外两个点坐标。

2) 在平面坐标系内任意两个点之间画一条指定曲率的曲线(arc)。在这个需求中,要计算arc,自然要知道radius, startAngle, endAngle,圆心坐标。可以根据曲率来计算出半径等,但是难点在计算圆心坐标。

 

这两个需求的共同特点是:

1)两个已知的点

2)根据这两个点和其他的条件去计算一些必须的(画line,arc等必须的)点坐标。

 

目前我遇到了这两种需求,都通过atan2来解决的。其他的情况,目前尚且未知,待后续发现时,补充上。

 

]]>
Netty自娱自乐之类Dubbo RPC 框架设计构想 【上篇】 http://doc.okbase.net/liferecord/archive/265413.html vOoT 2017/9/13 22:27:46

  之前在前一篇的《Netty自娱自乐之协议栈设计》,菜鸟我已经自娱自乐了设计协议栈,gitHub地址为https://github.com/vOoT/ncustomer-protocal。先这一篇中,准备接着自娱去实现一个RPC框架,现在公司共的是Dubbo,那么先不看其代码,先自行实现一下吧。

  dubbo 包括 注册和服务调用,细节我们先不管,然后,我先先实现一个如下的简单模型

     

   哈哈哈,第一个版本就是这么简单,粗暴。说到自定义配置,首先想到的是Spring 自定义标签,利用标签进行配置服务。而我设计的标签页非常的简单,使用如下:

    <rpc:provider id="helloServiceImpl" class="com.qee.rpc.HelloServiceImpl"/>

    <rpc:cumsumer id="helloService" interface="com.qee.rpc.HelloService"/>

看到了没,非常像dubbo,那么如何实现一个自定义标签呢,从网上可以了解搜索的到,现在我就简单说明一下,如何编写和测试自己自定义的Spring 标签。

  一、 定义xsd 文件,该文件是xml文件的 schema 定义。从上面的例子中,我们知道xsd文件里面应该有2个节点,1个provider节点和1个cumsumer节点定义。然后制定provider节点有id 和classs属性,而cumsumer节点有 id和 interface属性。定义文件如下(该文件名为light-weight-rpc.xsd):

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.qee.com/schema/rpc"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            xmlns:beans="http://www.springframework.org/schema/beans"
            targetNamespace="http://www.qee.com/schema/rpc"
            elementFormDefault="qualified"
            attributeFormDefault="unqualified">

    <xsd:import namespace="http://www.springframework.org/schema/beans"/>

    <xsd:element name="provider" type="rpc-provider-type"></xsd:element>

    <xsd:element name="cumsumer" type="rpc-cumsumer-type"></xsd:element>

    <xsd:complexType name="rpc-provider-type">
        <xsd:attribute name="id" type="xsd:string" use="required"></xsd:attribute>
        <xsd:attribute name="class" type="xsd:string" use="required"></xsd:attribute>
    </xsd:complexType>

    <xsd:complexType name="rpc-cumsumer-type">
        <xsd:attribute name="id" type="xsd:string" use="required"></xsd:attribute>
        <xsd:attribute name="interface" type="xsd:string" use="required"></xsd:attribute>
    </xsd:complexType>

</xsd:schema>

  上面,画上红线的地方需要注意和主要的关注点,首先需要说明这个文件的name space 为 xmlns="http://www.qee.com/schema/rpc 。其他的具体如何写可以到网上搜索。有了这个文件,我们需要在xml的文件引入他,比如如下test.xml文件如何引用该文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rpc
="http://www.qee.com/schema/rpc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.qee.com/schema/rpc http://www.qee.com/schema/rpc/light-weight-rpc.xsd"> <rpc:provider id="helloServiceImpl" class="com.qee.rpc.HelloServiceImpl"/> <rpc:cumsumer id="helloService" interface="com.qee.rpc.HelloService"/> </beans>

  上面就是一个spring xml 文件,主要关注的是花黄线的部分,这样就可以使用<rpc:provider> 和<rpc:cumsumer>。

  二、组织文件,即要把文件放到合适的地方,让Spring能够识别。第一步,需要把light-weight-rpc.xsd文件放到META-INF的文件夹下,然后在META-INF文件创建2个新的文件,名字固定。

文件1:spring.schemes ,该文件里面直有一行数据,如下

       http\://www.qee.com/schema/rpc/light-weight-rpc.xsd=META-INF/light-weight-rpc.xsd

  该行告诉Spring容器,http://www.qee.com/schema/rpc/light-weight-rpc.xsd ,之前定义命名空间的light-weight-rpc.xsd文件是META-INF下的light-weight-rpc.xsd

文件2:spring.handlers,该文件里面也只有一行数据,如下

       http\://www.qee.com/schema/rpc=com.qee.rpc.config.support.LightWeightRpcNamespaceHandlerSupport

  该行告诉Spring容器,命名空间http://www.qee.com/schema/rpc的解析处理器是 com.qee.rpc.config.support.LightWeightRpcNamespaceHandlerSupport。这个例子的目录如下

  

 

好了到现在我们基本把文件的位置放置正确了。之后就是需要编写com.qee.rpc.config.support.LightWeightRpcNamespaceHandlerSupport。

  三、编写com.qee.rpc.config.support.LightWeightRpcNamespaceHandlerSupport,该类需要继承NamespaceHandlerSupport,重写init()方法。主要的目的就是注册,节点解析处理器。

代码如下:

public class LightWeightRpcNamespaceHandlerSupport extends NamespaceHandlerSupport {

    @Override
    public void init() {
        //注册用于解析<rpc>的解析器
        registerBeanDefinitionParser("provider", new LightWeightRpcBeanDefinitionParser());
        registerBeanDefinitionParser("cumsumer", new LightWeightRpcBeanDefinitionParser());
    }
}

  从代码上我们只要,就是把解析xml文件provider和cumsumer节点进行BeanDefinition转化解析。

  因为这2个节点非常的类型。所以我就只想用痛一个解析处理器,LightWeightRpcBeanDefinitionParser,该转化器继承org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser。具体代码如下:

public class LightWeightRpcBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {


    protected Class getBeanClass(Element element) {
        return LightWeightRPCElement.class;
    }

    protected void doParse(Element element, BeanDefinitionBuilder bean) {
        String interfaces = element.getAttribute("interface");
        String clazz = element.getAttribute("class");
        String id = element.getAttribute("id");
        bean.addPropertyValue("id", id + "Config");
        if (StringUtils.hasText(id)) {
            bean.addPropertyValue("beanName", id);
        }
        if (StringUtils.hasText(clazz)) {
            bean.addPropertyValue("clazz", clazz);
        }
        if (StringUtils.hasText(interfaces)) {
            bean.addPropertyValue("interfaces", interfaces);
        }
    }

}

  我们把xml的id 放到 bean 的beanName,把id+"Config"放到 id上,因为这个 BeanDefinitionBuilder 最终生成的对象是 LightWeightRPCElement,不是我们需要的代码对象。

@Data
@ToString
public class LightWeightRPCElement {
  private String id;

  private String beanName;

  private String clazz;

  private String interfaces;
}

  是不是非常的简单,到目前为止,我们已经完成了所有的自定义标签工作,下一步当然就是测试一下啦,代码如下:

public class RPCTest {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("test.xml");
        LightWeightRPCElement p1= (LightWeightRPCElement)ctx.getBean("helloServiceImplConfig");
        LightWeightRPCElement p2= (LightWeightRPCElement)ctx.getBean("helloServiceConfig");
        System.out.println(p1);
        System.out.println(p2);

    }
}

执行结果是:

 

  四、这一步的话,我们需要处理之前已经注册到Spring的 LightWeightRPCElement 的对象,在上面的例子中,这两个的Bean Id分别是helloServiceImplConfig、helloServiceConfig,之后我们需要通过这2个对象来产生我们需要代理对象。首先我们来看一下JDK的生成代理对象的方法:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler invocationHandler);

从上面的代码中,我们知道生产一个代理对象需要一个类加载器loader,和代理接口的字节码interfaces,和代理处理具柄invocationHandler。那么我程序定义了一个名为InterfaceProxyHandler的代理处理具柄,它继承InvocationHandler。代码如下:
@Data
public class InterfaceProxyHandler implements InvocationHandler {


    private CallBackExcuteHandler excuteHandler;


    public InterfaceProxyHandler(CallBackExcuteHandler excuteHandler) {
        this.excuteHandler = excuteHandler;
    }


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        MessageCallback callback = ExcuteManager.invoke(excuteHandler);
        .......//这里代码还没写,其实就是处理返回结果,准备下章解决。
    }


}

  从上面的代码,我们知道,它具体的执行逻辑是invoke方法。具体内容就是通过一个ExcuteManager来处理逻辑,该ExcuteManager就是一个封装了ExecutorService的线程池管理类。其意思是每个代理对象去执行方法时,都是通过线程池的一个线程去执行,而这个线程池管理类的执行方法invoke需要一个Callable任务,所以程序自定义了一个CallBackExcuteHandler类。代码如下:

public class CallBackExcuteHandler implements Callable<MessageCallback> {


    private String beanName;

    private List<InetSocketAddress> remoteAddresses;


    private LoadBalancedStrategy loadBalancedStrategy;


    public CallBackExcuteHandler(String beanName) {
        this.beanName = beanName;
    }

    public CallBackExcuteHandler(String beanName, List<InetSocketAddress> remoteAddresses) {
        this.beanName = beanName;
        this.remoteAddresses = remoteAddresses;
    }

    public CallBackExcuteHandler(String beanName, List<InetSocketAddress> remoteAddresses, LoadBalancedStrategy loadBalancedStrategy) {
        this.beanName = beanName;
        this.remoteAddresses = remoteAddresses;
        this.loadBalancedStrategy = loadBalancedStrategy;
    }

    public CallBackExcuteHandler() {

    }

    /**
     * 线程执行
     *
     * @return
     * @throws Exception
     */
    @Override
    public MessageCallback call() throws Exception {
        if (CollectionUtils.isEmpty(remoteAddresses)) {
            List<ServiceAddressConfig> remoteUrls = ServiceRemoteUrlContext.getInstance().getRemoteUrls(beanName);
            if (CollectionUtils.isEmpty(remoteUrls)) {
                throw new RuntimeException("服务 [" + beanName + " ]远程地址错误");
            }
        }

        int size = remoteAddresses.size();

        int idx = loadBalancedStrategy.strategy(size);


        InetSocketAddress inetSocketAddress = remoteAddresses.get(idx);
        System.out.println("返回的地址" + inetSocketAddress + "  idx=" + idx);

        MessageCallback messageCallback = new MessageCallback();

        return messageCallback;
    }
}

  具体逻辑就是看call,这里就是处理的具体逻辑,这个逻辑其实就是处理Netty网络通信的内容,等下章开始讲解,这一章主要通过搭建具体的框架,之后补充细节。这里远程地址为空的话,去远程地址管理上下文获取,接着通过一个负载均衡策略对象,返回其中一个地址的index。通过这种方式实现负载均衡调用。

  远程地址管理上下文对象代码如下:

public class ServiceRemoteUrlContext {

    private Map<String, List<ServiceAddressConfig>> remoteUrls;

    private volatile static ServiceRemoteUrlContext context;


    private ServiceRemoteUrlContext() {

    }

    public static ServiceRemoteUrlContext getInstance() {
        if (context == null) {
            synchronized (ServiceRemoteUrlContext.class) {
                if (context == null) {
                    context = new ServiceRemoteUrlContext();
                    context.remoteUrls = new HashMap<>();
                }
            }
        }
        return context;
    }


    /**
     * 添加一个远程地址,地址从service-url.properties 获取
     *
     * @param beanName
     * @param serviceAddressConfig
     * @return
     */
    public boolean addServiceAddress(String beanName, ServiceAddressConfig serviceAddressConfig) {
        if (StringUtils.isEmpty(beanName) || serviceAddressConfig == null) {
            return false;
        }
        synchronized (remoteUrls) {
            if (remoteUrls.get(beanName) == null) {
                List<ServiceAddressConfig> remoteAddress = new ArrayList<>();
                remoteAddress.add(serviceAddressConfig);
                remoteUrls.put(beanName, remoteAddress);
            } else {
                List<ServiceAddressConfig> serviceAddressConfigs = remoteUrls.get(beanName);
                if (serviceAddressConfigs.contains(serviceAddressConfig)) {
                    return false;
                }
                serviceAddressConfigs.add(serviceAddressConfig);
                return true;
            }
        }
        return false;
    }

    /**
     * 获取一个服务的远程地址 ,beanName like "com.qee.rpc.config.test.HelloService"
     *
     * @param beanName
     * @return
     */
    public List<ServiceAddressConfig> getRemoteUrls(String beanName) {
        return remoteUrls.get(beanName);
    }


}

  负载均衡的接口,代码如下:

public interface LoadBalancedStrategy {

    /**
     * 从 0 -size-1 获取一个值
     *
     * @param size
     * @return
     */
    int strategy(int size);
}

  现在只实现了1中,轮询方法,之后可以写成可配置,代码如下:

public class RollPolingStrategy implements LoadBalancedStrategy {

    private int currentValue = 0;

    private Class<?> clazz;

    public RollPolingStrategy(Class<?> clazz) {
        this.clazz = clazz;
    }

    @Override
    public int strategy(int size) {
        synchronized (clazz) {
            int nextValue = (currentValue + 1) % size;
            currentValue = nextValue;
            if (currentValue > size) {
                nextValue = 0;
            }
            return currentValue;
        }
    }
}

  接着,我们需要看一下简单的ExcuteManager类,代码如下:

public class ExcuteManager {

    /**
     * 默认是200个线程
     */
    private static final int DEFAULT_THRED_NUM = 200;

    /**
     * 超时时间为1秒
     */
    private static final int DEFAULT_TIME_OUT_TIME = 1000;

    private static ExecutorService executorService = Executors.newFixedThreadPool(DEFAULT_THRED_NUM);

    public static MessageCallback invoke(Callable<MessageCallback> call) {
        Future<MessageCallback> submit = executorService.submit(call);
        try {
            return submit.get(DEFAULT_TIME_OUT_TIME, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            submit.cancel(true);
            throw new RuntimeException("the method is interupted ", e);
        } catch (ExecutionException e) {
            submit.cancel(true);
            throw new RuntimeException("the method cal excute exception", e);
        } catch (TimeoutException e) {
            System.out.println(Thread.currentThread().getName());
            submit.cancel(true);
            throw new RuntimeException("the method call is time out  ", e);
        }
    }

    public static void shutdown() {
        executorService.shutdown();
    }

    public static void shutdownNow() {
        executorService.shutdownNow();
    }

}

  这些参数,在后面都做成可配置的。

  最后一步了,就是需要生产一个代理对象,并把代理对象注册到Spring容器里面。那么Spring的 BeanPostProcessor可以为我们解决问题,看代码如下:

@Component
public class RegisterRpcProxyBeanProcessor implements BeanPostProcessor, BeanFactoryAware {


    private BeanFactory beanFactory;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        Object target = bean;
        if (bean instanceof LightWeightRPCElement) {
        //如果是LightWeightRPCElement,则强转,否则不处理
            LightWeightRPCElement rpcElement = (LightWeightRPCElement) bean;
            
           // 接着就是获取 之前XML 的属性值
            Class<?> clazz = null;
            if (!StringUtils.isEmpty(rpcElement.getInterfaces())) {
                try {
                    clazz = Class.forName(rpcElement.getInterfaces());
                } catch (ClassNotFoundException e) {
                    throw new RuntimeException("获取 [" + rpcElement.getInterfaces() + " ] class字节码失败");
                }
            }
             //通过ServiceRemoteUrlContext得到这个接口的远程端口和地址
            List<ServiceAddressConfig> remoteUrls = ServiceRemoteUrlContext.getInstance().getRemoteUrls(rpcElement.getInterfaces());
            List<InetSocketAddress> remoteAddressList = ExtractUtil.extractList(remoteUrls, "remoteAddress", ServiceAddressConfig.class);
            CallBackExcuteHandler callBackExcuteHandler = new CallBackExcuteHandler(rpcElement.getInterfaces(), remoteAddressList,new RollPolingStrategy(clazz));

            InterfaceProxyHandler interfaceProxyHandler = new InterfaceProxyHandler(callBackExcuteHandler);
            //这里之后可以优化为各种方式产生动态代理,如cglib等
            target = Proxy.newProxyInstance(bean.getClass().getClassLoader(), new Class[]{clazz}, interfaceProxyHandler);
            if (beanFactory instanceof DefaultListableBeanFactory) {
                //这里就是动态注册对象,把动态代理对象注册到Spring上
                DefaultListableBeanFactory defaultFactory = (DefaultListableBeanFactory) beanFactory;
                defaultFactory.registerSingleton(rpcElement.getBeanName(), target);
            }
        }
        return target;
    }


    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }
     

  从上面的注释大家应该也非常的清楚了,现在只剩下最后一步了,如何获取该接口的远程服务地址和端口,dubbo是通过注册中心zookeeper,而这里的简单的采用配置,例子如下:

   com.qee.rpc.config.test.HelloService 127.0.0.1:8888,127.0.0.1:7777,127.0.0.1:9999

  对,就是在一个properties文件上 通过服务接口全称 和指定远程服务主机和端口。之后可以改为有注册中心的方式。现在我们来看一下读取这个配置的类,代码如下:

@Component
public class ServiceRemoteUrlsInit implements InitializingBean {

    /**
     * 远程服务配置地址路径,默认
     */
    @Value("${remote-urls-path:classpath:service-urls.properties}")
    private String remoteUrlsPropertyPath;

    @Override
    public void afterPropertiesSet() throws Exception {
        Properties pps = new Properties();
        if (!remoteUrlsPropertyPath.startsWith("classpath")) {
            throw new RuntimeException(remoteUrlsPropertyPath + "不存在");
        }
        String[] filePath = remoteUrlsPropertyPath.split(":");
        if (filePath == null || filePath.length != 2) {
            throw new RuntimeException(remoteUrlsPropertyPath + "内容配置错误");
        }
        ClassPathResource resource = new ClassPathResource(filePath[1]);
        InputStream in = new BufferedInputStream(resource.getInputStream());
        pps.load(in);
        Enumeration en = pps.propertyNames();

        while (en.hasMoreElements()) {
            String beanName = (String) en.nextElement();
            String strRemoteUrls = pps.getProperty(beanName);
            String[] remoteUrls = strRemoteUrls.split(",");
            if (remoteUrls == null || remoteUrls.length == 0) {
                break;
            }
            for (String remoteUrl : remoteUrls) {
                String[] hostPort = remoteUrl.split(":");
                if (hostPort == null || hostPort.length != 2) {
                    throw new RuntimeException(remoteUrlsPropertyPath + " 配置内容错误");
                }
                ServiceAddressConfig serviceAddressConfig = new ServiceAddressConfig();
                serviceAddressConfig.setBeanName(beanName);
                serviceAddressConfig.setHostName(hostPort[0]);
                serviceAddressConfig.setRemotePort(Integer.valueOf(hostPort[1]));
                InetSocketAddress socketAddress = new InetSocketAddress(serviceAddressConfig.getHostName(), serviceAddressConfig.getRemotePort());
                serviceAddressConfig.setRemoteAddress(socketAddress);
                ServiceRemoteUrlContext.getInstance().addServiceAddress(beanName, serviceAddressConfig);
            }

        }

    }
}

  代码比较简单,就是实现 InitializingBean这个Spring接口,Spring启动在Bean创建后,初始化 afterPropertiesSet()这个配置,在这个方法里面读取类路径的配置文件。最后我们来运行一个例子。还是HelloService.我们有一个Invoker类,需要注入HelloService 对象调用。代码如下:

@Component
public class Invoker {

    @Autowired
    private HelloService helloService;

    @Resource(name = "helloService")
    private HelloService helloService2;

    public void print() {
        helloService.hello("123");
        helloService2.hello("122344");

    }
}

然后通过SpringBoot 启动测试:

@ComponentScan(basePackages = "com.qee.rpc")
@EnableAutoConfiguration
public class App {

    private static ExecutorService executorService = Executors.newCachedThreadPool();


    private static final CountDownLatch cd = new CountDownLatch(1);


    public static void main(String[] args) {

        try {
            SpringApplication.run(App.class, args);
            System.out.println("the main Thread :" + Thread.currentThread().getName());
            final Invoker invoker = (Invoker) ApplicationContextUtils.getBean("invoker");
            for (int i = 0; i < 300; i++) {
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            cd.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        invoker.print();
                    }
                });
            }


            cd.countDown();

            Thread.sleep(100000);

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            ExcuteManager.shutdown();
            executorService.shutdown();
        }

    }

  有300个线程去调这个 invoker.print();修改一下 InterfaceProxyHandler的invoke方法,因为我们底层的通信还没完成。所以以

    System.out.println("在InterfaceProxyHandler上调用invoke方法,参数是=" + args[0]);

  以这个语句来测试一下代码,其中这个大致框架已经上传到gitHub:https://github.com/vOoT/light-weight-rpc, 有什么建议和问题,大家一起讨论吧。最后贴一下执行结果:

  哈哈哈,这样我们是不是就是可以通过Spring注解 @Autowired 和 @Resource 来注入动态对象。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  





















 

]]>
angularjs-1.3代码学习-$parse http://doc.okbase.net/wumadi/archive/265412.html 无码帝 2017/9/13 22:27:40

这次我们来看一下angular的Sandboxing Angular Expressions。关于内置方法的,核心有两块:Lexer和Parser。其中大家对$parse可能更了解一点。好了不多废话,先看Lexer的内部结构:

1.Lexer

//构造函数
var Lexer = function(options) {
  this.options = options;
};
//原型 
Lexer.prototype = {
    constructor: Lexer,
    lex: function(){},
    is: function(){},
    peek: function(){ /* 返回表达式的下一个位置的数据,如果没有则返回false */ },
    isNumber: function(){ /* 判断当前表达式是否是一个数字 */ },
    isWhitespace: function(){/* 判断当前表达式是否是空格符 */},
    isIdent: function(){/* 判断当前表达式是否是英文字符(包含_和$) */},
    isExpOperator: function(){/* 判断当时表达式是否是-,+还是数字 */},
    throwError: function(){ /* 抛出异常 */},
    readNumber: function(){ /* 读取数字 */},
    readIdent: function(){ /* 读取字符 */},
    readString: function(){ /*读取携带''或""的字符串*/ }
};

 这里指出一点,因为是表达式。所以类似"123"这类的东西,在Lexer看来应该算是数字而非字符串。表达式中的字符串必须使用单引号或者双引号来标识。Lexer的核心逻辑在lex方法中:

lex: function(text) {
    this.text = text;
    this.index = 0;
    this.tokens = [];

    while (this.index < this.text.length) {
      var ch = this.text.charAt(this.index);
      if (ch === '"' || ch === "'") {
        /* 尝试判断是否是字符串 */
        this.readString(ch);
      } else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) {
        /* 尝试判断是否是数字 */
        this.readNumber();
      } else if (this.isIdent(ch)) {
        /* 尝试判断是否是字母 */
        this.readIdent();
      } else if (this.is(ch, '(){}[].,;:?')) {
        /* 判断是否是(){}[].,;:? */
        this.tokens.push({index: this.index, text: ch});
        this.index++;
      } else if (this.isWhitespace(ch)) {
        /* 判断是否是空白符 */
        this.index++;
      } else {
        /* 尝试匹配操作运算 */
        var ch2 = ch + this.peek();
        var ch3 = ch2 + this.peek(2);
        var op1 = OPERATORS[ch];
        var op2 = OPERATORS[ch2];
        var op3 = OPERATORS[ch3];
        if (op1 || op2 || op3) {
          var token = op3 ? ch3 : (op2 ? ch2 : ch);
          this.tokens.push({index: this.index, text: token, operator: true});
          this.index += token.length;
        } else {
          this.throwError('Unexpected next character ', this.index, this.index + 1);
        }
      }
    }
    return this.tokens;
  }

主要看一下匹配操作运算。这里源码中会调用OPERATORS。看一下OPERATORS:

var OPERATORS = extend(createMap(), {
    '+':function(self, locals, a, b) {
      a=a(self, locals); b=b(self, locals);
      if (isDefined(a)) {
        if (isDefined(b)) {
          return a + b;
        }
        return a;
      }
      return isDefined(b) ? b : undefined;},
    '-':function(self, locals, a, b) {
          a=a(self, locals); b=b(self, locals);
          return (isDefined(a) ? a : 0) - (isDefined(b) ? b : 0);
        },
    '*':function(self, locals, a, b) {return a(self, locals) * b(self, locals);},
    '/':function(self, locals, a, b) {return a(self, locals) / b(self, locals);},
    '%':function(self, locals, a, b) {return a(self, locals) % b(self, locals);},
    '===':function(self, locals, a, b) {return a(self, locals) === b(self, locals);},
    '!==':function(self, locals, a, b) {return a(self, locals) !== b(self, locals);},
    '==':function(self, locals, a, b) {return a(self, locals) == b(self, locals);},
    '!=':function(self, locals, a, b) {return a(self, locals) != b(self, locals);},
    '<':function(self, locals, a, b) {return a(self, locals) < b(self, locals);},
    '>':function(self, locals, a, b) {return a(self, locals) > b(self, locals);},
    '<=':function(self, locals, a, b) {return a(self, locals) <= b(self, locals);},
    '>=':function(self, locals, a, b) {return a(self, locals) >= b(self, locals);},
    '&&':function(self, locals, a, b) {return a(self, locals) && b(self, locals);},
    '||':function(self, locals, a, b) {return a(self, locals) || b(self, locals);},
    '!':function(self, locals, a) {return !a(self, locals);},

    //Tokenized as operators but parsed as assignment/filters
    '=':true,
    '|':true
});

可以看到OPERATORS实际上存储的是操作符和操作符函数的键值对。根据操作符返回对应的操作符函数。我们看一下调用例子:

var _l = new Lexer({});
var a = _l.lex("a = a + 1");
console.log(a);

 结合之前的lex方法,我们来回顾下代码执行过程:

1.index指向'a'是一个字母。匹配isIdent成功。将生成的token存入tokens中

2.index指向空格符,匹配isWhitespace成功,同上

3.index指向=,匹配操作运算符成功,同上

4.index指向空格符,匹配isWhitespace成功,同上

5.index指向'a'是一个字母。匹配isIdent成功。同上

7.index指向+,匹配操作运算符成功,同上

8.index指向空格符,匹配isWhitespace成功,同上

9.index指向1,匹配数字成功,同上

以上则是"a = a + 1"的代码执行过程。9步执行结束之后,跳出while循环。刚才我们看到了,每次匹配成功,源码会生成一个token。因为匹配类型的不同,生成出来的token的键值对略有不同:

number:{
      index: start,
      text: number,
      constant: true,
      value: Number(number)
    },
string: {
          index: start,
          text: rawString,
          constant: true,
          value: string
        },
ident: {
      index: start,
      text: this.text.slice(start, this.index),
      identifier: true /* 字符表示 */ 
    },
'(){}[].,;:?': {
    index: this.index,
    text: ch
},
"操作符": {
     index: this.index, 
     text: token, 
     operator: true
}
//text是表达式,而value才是实际的值

number和string其实都有相对应的真实值,意味着如果我们表达式是2e2,那number生成的token的值value就应该是200。到此我们通过lexer类获得了一个具有token值得数组。从外部看,实际上Lexer是将我们输入的表达式解析成了token json。可以理解为生成了表达式的语法树(AST)。但是目前来看,我们依旧还没有能获得我们定义表达式的结果。那就需要用到parser了。

2.Parser

先看一下Parser的内部结构:

//构造函数
var Parser = function(lexer, $filter, options) {
  this.lexer = lexer;
  this.$filter = $filter;
  this.options = options;
};

//原型
Parser.prototype = {
  constructor: Parser,
  parse: function(){},
  primary: function(){},
  throwError: function(){ /* 语法抛错 */},
  peekToken: function(){},
  peek: function(){/*返回tokens中的第一个成员对象 */},
  peekAhead: function(){ /* 返回tokens中指定成员对象,否则返回false */},
  expect: function(){ /* 取出tokens中第一个对象,否则返回false */ },
  consume: function(){ /* 取出第一个,底层调用expect */ },
  unaryFn: function(){ /* 一元操作 */},
  binaryFn: function(){ /* 二元操作 */},
  identifier: function(){},
  constant: function(){},
  statements: function(){},
  filterChain: function(){},
  filter: function(){},
  expression: function(){},
  assignment: function(){},
  ternary: function(){},
  logicalOR: function(){ /* 逻辑或 */},
  logicalAND: function(){ /* 逻辑与 */ },
  equality: function(){ /* 等于 */ },
  relational: function(){ /* 比较关系 */ },
  additive: function(){ /* 加法,减法 */ },
  multiplicative: function(){ /* 乘法,除法,求余 */ },
  unary: function(){ /* 一元 */ },
  fieldAccess: function(){},
  objectIndex: function(){},
  functionCall: function(){},
  arrayDeclaration: function(){},
  object: function(){}
}

Parser的入口方法是parse,内部执行了statements方法。来看下statements:

statements: function() {
    var statements = [];
    while (true) {
      if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']'))
        statements.push(this.filterChain());
      if (!this.expect(';')) {
        // optimize for the common case where there is only one statement.
        // TODO(size): maybe we should not support multiple statements?
        return (statements.length === 1)
            ? statements[0]
            : function $parseStatements(self, locals) {
                var value;
                for (var i = 0, ii = statements.length; i < ii; i++) {
                  value = statements[i](self, locals);
                }
                return value;
              };
      }
    }
  }

这里我们将tokens理解为表达式,实际上它就是经过表达式通过lexer转换过来的。statements中。如果表达式不以},),;,]开头,将会执行filterChain方法。当tokens检索完成之后,最后返回了一个$parseStatements方法。其实Parser中很多方法都返回了类似的对象,意味着返回的内容将需要执行后才能得到结果。

看一下filterChain:

filterChain: function() {
    /* 针对angular语法的filter */
    var left = this.expression();
    var token;
    while ((token = this.expect('|'))) {
      left = this.filter(left);
    }
    return left;
  }

其中filterChain是针对angular表达式独有的"|"filter写法设计的。我们先绕过这块,进入expression

expression: function() {
    return this.assignment();
  }

再看assignment:

assignment: function() {
    var left = this.ternary();
    var right;
    var token;
    if ((token = this.expect('='))) {
      if (!left.assign) {
        this.throwError('implies assignment but [' +
            this.text.substring(0, token.index) + '] can not be assigned to', token);
      }
      right = this.ternary();
      return extend(function $parseAssignment(scope, locals) {
        return left.assign(scope, right(scope, locals), locals);
      }, {
        inputs: [left, right]
      });
    }
    return left;
  }

我们看到了ternary方法。这是一个解析三目操作的方法。与此同时,assignment将表达式以=划分成left和right两块。并且两块都尝试执行ternary。

ternary: function() {
    var left = this.logicalOR();
    var middle;
    var token;
    if ((token = this.expect('?'))) {
      middle = this.assignment();
      if (this.consume(':')) {
        var right = this.assignment();

        return extend(function $parseTernary(self, locals) {
          return left(self, locals) ? middle(self, locals) : right(self, locals);
        }, {
          constant: left.constant && middle.constant && right.constant
        });
      }
    }

    return left;
  }

在解析三目运算之前,又根据?将表达式划分成left和right两块。左侧再去尝试执行logicalOR,实际上这是一个逻辑与的解析,按照这个执行流程,我们一下有了思路。这有点类似我们一般写三目时。代码的执行情况,比如: 2 > 2 ? 1 : 0。如果把这个当成表达式,那根据?划分left和right,left就应该是2 > 2,right应该就是 1: 0。然后尝试在left看是否有逻辑或的操作。也就是,Parser里面的方法调用的嵌套级数越深,其方法的优先级则越高。好,那我们一口气看看这个最高的优先级在哪?

logicalOR -> logicalAND -> equality -> relational -> additive -> multiplicative ->  unary

好吧,嵌套级数确实有点多。那么我们看下unary。

unary: function() {
    var token;
    if (this.expect('+')) {
      return this.primary();
    } else if ((token = this.expect('-'))) {
      return this.binaryFn(Parser.ZERO, token.text, this.unary());
    } else if ((token = this.expect('!'))) {
      return this.unaryFn(token.text, this.unary());
    } else {
      return this.primary();
    }
  }

这边需要看两个主要的方法,一个是binaryFn和primay。如果判断是-,则必须通过binaryFn去添加函数。看下binaryFn

binaryFn: function(left, op, right, isBranching) {
    var fn = OPERATORS[op];
    return extend(function $parseBinaryFn(self, locals) {
      return fn(self, locals, left, right);
    }, {
      constant: left.constant && right.constant,
      inputs: !isBranching && [left, right]
    });
  }

其中OPERATORS是之前聊Lexer也用到过,它根据操作符存储相应的操作函数。看一下fn(self, locals, left, right)。而我们随便取OPERATORS中的一个例子:

'-':function(self, locals, a, b) {
          a=a(self, locals); b=b(self, locals);
          return (isDefined(a) ? a : 0) - (isDefined(b) ? b : 0);
        }

其中a和b就是left和right,他们其实都是返回的跟之前类似的$parseStatements方法。默认存储着token中的value。经过事先解析好的四则运算来生成最终答案。其实这就是Parser的基本功能。至于嵌套,我们可以把它理解为js的操作符的优先级。这样就一目了然了。至于primay方法。塔刷选{ ( 对象做进一步的解析过程。

Parser的代码并不复杂,只是函数方法间调用密切,让我们再看一个例子:

var _l = new Lexer({});
var _p = new Parser(_l);
var a = _p.parse("1 + 1 + 2");
console.log(a()); //4

我们看下1+1+2生成的token是什么样的:

[
{"index":0,"text":"1","constant":true,"value":1},{"index":2,"text":"+","operator":true},{"index":4,"text":"1","constant":true,"value":1},{"index":6,"text":"+","operator":true},{"index":8,"text":"2","constant":true,"value":2}
]

Parser根据lexer生成的tokens尝试解析。tokens每一个成员都会生成一个函数,其先后执行逻辑按照用户输入的1+1+2的顺序执行。注意像1和2这类constants为true的token,parser会通过constant生成需要的函数$parseConstant,也就是说1+1+2中的两个1和一个2都是返回$parseConstant函数,通过$parseBinaryFn管理加法逻辑。

constant: function() {
    var value = this.consume().value;

    return extend(function $parseConstant() {
      return value; //这个函数执行之后,就是将value值返回。
    }, {
      constant: true,
      literal: true
    });
  },
binaryFn: function(left, op, right, isBranching) {
    var fn = OPERATORS[op];//加法逻辑
    return extend(function $parseBinaryFn(self, locals) {
      return fn(self, locals, left, right);//left和right分别表示生成的对应函数
    }, {
      constant: left.constant && right.constant,
      inputs: !isBranching && [left, right]
    });
  }

那我们demo中的a应该返回什么函数呢?当然是$parseBinaryFn。其中的left和right分别是1+1的$parseBinaryFn,right就是2的$parseConstant。

再来一个例子:

var _l = new Lexer({});
var _p = new Parser(_l);
var a = _p.parse('{"name": "hello"}');
console.log(a);

这边我们传入一个json,理论上我们执行完a函数,应该返回一个{name: "hello"}的对象。它调用了Parser中的object

object: function() {
    var keys = [], valueFns = [];
    if (this.peekToken().text !== '}') {
      do {
        if (this.peek('}')) {
          // Support trailing commas per ES5.1.
          break;
        }
        var token = this.consume();
        if (token.constant) {
          //把key取出来
          keys.push(token.value);
        } else if (token.identifier) {
          keys.push(token.text);
        } else {
          this.throwError("invalid key", token);
        }
        this.consume(':');
        //冒号之后,则是值,将值存在valueFns中
        valueFns.push(this.expression());
        //根据逗号去迭代下一个
      } while (this.expect(','));
    }
    this.consume('}');

    return extend(function $parseObjectLiteral(self, locals) {
      var object = {};
      for (var i = 0, ii = valueFns.length; i < ii; i++) {
        object[keys[i]] = valueFns[i](self, locals);
      }
      return object;
    }, {
      literal: true,
      constant: valueFns.every(isConstant),
      inputs: valueFns
    });
  }

比方我们的例子{"name": "hello"},object会将name存在keys中,hello则会生成$parseConstant函数存在valueFns中,最终返回$parseObjectLiternal函数。

下一个例子:

var a = _p.parse('{"name": "hello"}["name"]');

这个跟上一个例子的差别在于后面尝试去读取name的值,这边则调用parser中的objectIndex方法。

objectIndex: function(obj) {
    var expression = this.text;

    var indexFn = this.expression();
    this.consume(']');

    return extend(function $parseObjectIndex(self, locals) {
      var o = obj(self, locals), //parseObjectLiteral,实际就是obj
          i = indexFn(self, locals), //$parseConstant,这里就是name
          v;

      ensureSafeMemberName(i, expression);
      if (!o) return undefined;
      v = ensureSafeObject(o[i], expression);
      return v;
    }, {
      assign: function(self, value, locals) {
        var key = ensureSafeMemberName(indexFn(self, locals), expression);
        // prevent overwriting of Function.constructor which would break ensureSafeObject check
        var o = ensureSafeObject(obj(self, locals), expression);
        if (!o) obj.assign(self, o = {}, locals);
        return o[key] = value;
      }
    });
  }

很简单吧,obj[xx]和obj.x类似。大家自行阅读,我们再看一个函数调用的demo

var _l = new Lexer({});
var _p = new Parser(_l, '', {});
var demo = {
  "test": function(){
    alert("welcome");
  }
};
var a = _p.parse('test()');
console.log(a(demo));

我们传入一个test的调用。这边调用了parser中的functionCall方法和identifier方法

identifier: function() {
    var id = this.consume().text;

    //Continue reading each `.identifier` unless it is a method invocation
    while (this.peek('.') && this.peekAhead(1).identifier && !this.peekAhead(2, '(')) {
      id += this.consume().text + this.consume().text;
    }

    return getterFn(id, this.options, this.text);
  }

看一下getterFn方法

...
forEach(pathKeys, function(key, index) {
      ensureSafeMemberName(key, fullExp);
      var lookupJs = (index
                      // we simply dereference 's' on any .dot notation
                      ? 's'
                      // but if we are first then we check locals first, and if so read it first
                      : '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '.' + key;
      if (expensiveChecks || isPossiblyDangerousMemberName(key)) {
        lookupJs = 'eso(' + lookupJs + ', fe)';
        needsEnsureSafeObject = true;
      }
      code += 'if(s == null) return undefined;\n' +
              's=' + lookupJs + ';\n';
    });
    code += 'return s;';

    /* jshint -W054 */
    var evaledFnGetter = new Function('s', 'l', 'eso', 'fe', code); // s=scope, l=locals, eso=ensureSafeObject
    /* jshint +W054 */
    evaledFnGetter.toString = valueFn(code);
...

这是通过字符串创建一个匿名函数的方法。我们看下demo的test生成了一个什么匿名函数:

function('s', 'l', 'eso', 'fe'){
if(s == null) return undefined;
s=((l&&l.hasOwnProperty("test"))?l:s).test;
return s;
}

这个匿名函数的意思,需要传入一个上下文,匿名函数通过查找上下文中是否有test属性,如果没有传上下文则直接返回未定义。这也就是为什么我们在生成好的a函数在执行它时需要传入demo对象的原因。最后补一个functionCall

functionCall: function(fnGetter, contextGetter) {
    var argsFn = [];
    if (this.peekToken().text !== ')') {
      /* 确认调用时有入参 */
      do {
        //形参存入argsFn
        argsFn.push(this.expression());
      } while (this.expect(','));
    }
    this.consume(')');

    var expressionText = this.text;
    // we can safely reuse the array across invocations
    var args = argsFn.length ? [] : null;

    return function $parseFunctionCall(scope, locals) {
      var context = contextGetter ? contextGetter(scope, locals) : isDefined(contextGetter) ? undefined : scope;
      //或者之前创建生成的匿名函数
      var fn = fnGetter(scope, locals, context) || noop;

      if (args) {
        var i = argsFn.length;
        while (i--) {
          args[i] = ensureSafeObject(argsFn[i](scope, locals), expressionText);
        }
      }

      ensureSafeObject(context, expressionText);
      ensureSafeFunction(fn, expressionText);

      // IE doesn't have apply for some native functions
      //执行匿名函数的时候需要传入上下文
      var v = fn.apply
            ? fn.apply(context, args)
            : fn(args[0], args[1], args[2], args[3], args[4]);

      if (args) {
        // Free-up the memory (arguments of the last function call).
        args.length = 0;
      }

      return ensureSafeObject(v, expressionText);
      };
  }

下面我们看一下$ParseProvider,这是一个基于Lex和Parser函数的angular内置provider。它对scope的api提供了基础支持。

...
return function $parse(exp, interceptorFn, expensiveChecks) {
      var parsedExpression, oneTime, cacheKey;

      switch (typeof exp) {
        case 'string':
          cacheKey = exp = exp.trim();

          var cache = (expensiveChecks ? cacheExpensive : cacheDefault);
          parsedExpression = cache[cacheKey];

          if (!parsedExpression) {
            if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
              oneTime = true;
              exp = exp.substring(2);
            }

            var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions;
            //调用lexer和parser
            var lexer = new Lexer(parseOptions);
            var parser = new Parser(lexer, $filter, parseOptions);
            parsedExpression = parser.parse(exp);
            //添加$$watchDelegate,为scope部分提供支持
            if (parsedExpression.constant) {
              parsedExpression.$$watchDelegate = constantWatchDelegate;
            } else if (oneTime) {
              //oneTime is not part of the exp passed to the Parser so we may have to
              //wrap the parsedExpression before adding a $$watchDelegate
              parsedExpression = wrapSharedExpression(parsedExpression);
              parsedExpression.$$watchDelegate = parsedExpression.literal ?
                oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
            } else if (parsedExpression.inputs) {
              parsedExpression.$$watchDelegate = inputsWatchDelegate;
            }
            //做相关缓存
            cache[cacheKey] = parsedExpression;
          }
          return addInterceptor(parsedExpression, interceptorFn);

        case 'function':
          return addInterceptor(exp, interceptorFn);

        default:
          return addInterceptor(noop, interceptorFn);
      }
    };

总结:Lexer和Parser的实现确实让我大开眼界。通过这两个函数,实现了angular自己的语法解析器。逻辑部分还是相对复杂

时间不多,内容刚好,以上是个人阅读源码的一些理解,有不对或者偏差的地方,还希望园友们斧正。共同进步。

]]>
【全面总结】js获取元素位置大小 http://doc.okbase.net/Nirvana-zsy/archive/265411.html Nirvana_zsy 2017/9/13 22:27:32
目录

1.关于offset
 
offsetParent(只读)
HTMLElement.offsetParent 是一个只读属性,返回一个指向最近的(closest,指包含层级上的最近)包含该元素的定位元素。如果没有定位的元素,则 offsetParent 为最近的 table, table cell 或根元素(标准模式下为 html;quirks 模式下为 body)。当元素的 style.display 设置为 "none" 时,offsetParent 返回 null。offsetParent 很有用,因为 offsetTop 和 offsetLeft 都是相对于其内边距边界的。
 
兼容性:
在 Webkit 中,如果元素为隐藏的(该元素或其祖先元素的 style.display 为 "none"),或者该元素的 style.position 被设为 "fixed",则该属性返回 null。
在 IE 9 中,如果该元素的 style.position 被设置为 "fixed",则该属性返回 null。(display:none 无影响。)
 
块级元素&行内元素
对块级元素来说,offsetTop、offsetLeft、offsetWidth 及 offsetHeight 描述了元素相对于 offsetParent 的边界框。
 
然而,对于可被截断到下一行的行内元素(如 span),offsetTop 和 offsetLeft 描述的是第一个边界框的位置(使用 Element.getClientRects() 来获取其宽度和高度),而 offsetWidth 和 offsetHeight 描述的是边界框的维度(使用 Element.getBoundingClientRect 来获取其位置)。因此,使用 offsetLeft、offsetTop、offsetWidth、offsetHeight 来对应 left、top、width 和 height 的一个盒子将不会是文本容器 span 的盒子边界。
 
offsetTop(只读)
HTMLElement.offsetTop 为只读属性,它返回当前元素相对于其 offsetParent 元素的顶部的距离。
 
offsetLeft(只读)
HTMLElement.offsetLeft 是一个只读属性,返回当前元素左上角相对于 HTMLElement.offsetParent 节点的左边界偏移的像素值。
 
offsetHeight(只读)
HTMLElement.offsetHeight 是一个只读属性,它返回该元素的像素高度,高度包含该元素的垂直内边距和边框,且是一个整数。
通常,元素的offsetHeight是一种元素CSS高度的衡量标准,包括元素的边框、内边距和元素的水平滚动条(如果存在且渲染的话),不包含:before或:after等伪类元素的高度。
对于文档的body对象,它包括代替元素的CSS高度线性总含量高。浮动元素的向下延伸内容高度是被忽略的。
 
 
offsetWidth(只读)
是?
HTMLElement.offsetWidth 是一个只读属性,返回一个元素的布局宽度。一个典型的(译者注:各浏览器的offsetWidth可能有所不同)offsetWidth是测量包含元素的边框(border)、水平线上的内边距(padding)、竖直方向滚动条(scrollbar)(如果存在的话)、以及CSS设置的宽度(width)的值。
 
 
2.滚动尺寸scroll
可以设置这些值来控制滚动
 
scrollWidth(只读)
元素的scrollWidth只读属性以px为单位返回元素的内容区域宽度或元素的本身的宽度中更大的那个值。若元素的宽度大于其内容的区域(例如,元素存在滚动条时), scrollWidth的值要大于clientWidth
 
scrollHeight(只读)
Element.scrollHeight 是计量元素内容高度的只读属性,包括overflow样式属性导致的视图中不可见内容。没有垂直滚动条的情况下,scrollHeight值与元素视图填充所有内容所需要的最小值clientHeight相同。包括元素的padding,但不包括元素的margin.
 
scrollLeft(可写)
Element.scrollLeft 属性可以读取或设置元素滚动条到元素左边的距离。
注意如果这个元素的内容排列方向(direction) 是rtl (right-to-left) ,那么滚动条会位于最右侧(内容开始处),并且scrollLeft值为0。此时,当你从右到左拖动滚动条时,scrollLeft会从0变为负数(这个特性在chrome浏览器中不存在)。
 
scrollTop(可写)
这个Element.scrollTop 属性可以设置或者获取一个元素被卷起的像素距离。一个元素的 scrollTop 是可以去计算出这个元素最高高度距离它容器顶部的可见高度。当一个元素的容器没有产生垂直方向的滚动条,那它的 scrollTop 的值默认为0.
 
注意:
获取页面向上滚动的距离,有浏览器兼容性问题,获取方法:
document.documentElement.scrollTop||document.body.scrollTop
 
3.关于client
clientWidth(只读)
Element.clientWidth 属性表示元素的内部宽度,以像素计。该属性包括内边距,但不包括垂直滚动条(如果有)、边框和外边距。
 
clentHeight(只读)
返回元素内部的高度(单位像素),包含内边距,但不包括水平滚动条、边框和外边距。
clientHeight 可以通过 CSS height + CSS padding - 水平滚动条高度 (如果存在)来计算.
 
获取页面高度:document.documentElement.clientHeight
 
clientLeft(只读)
表示一个元素的左边框的宽度,以像素表示。如果元素的文本方向是从右向左(RTL, right-to-left),并且由于内容溢出导致左边出现了一个垂直滚动条,则该属性包括滚动条的宽度。clientLeft 不包括左外边距和左内边距。clientLeft 是只读的。
 
clientTop(只读)
一个元素顶部边框的宽度(以像素表示)。不包括顶部外边距或内边距。clientTop 是只读的。
 
4.关于clientRect
getBoundingClientRect
Element.getBoundingClientRect()方法返回元素的大小及其相对于视口的位置。
返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects() 方法返回的一组矩形的集合, 即:是与该元素相关的CSS 边框集合 。
DOMRect 对象包含了一组用于描述边框的只读属性——left、top、right和bottom,单位为像素。除了 width 和 height 外的属性都是相对于视口的左上角位置而言的
当计算边界矩形时,会考虑视口区域(或其他可滚动元素)内的滚动操作,也就是说,当滚动位置发生了改变,top和left属性值就会随之立即发生变化(因此,它们的值是相对于视口的,而不是绝对的)。
 
兼容性:
意外的好啊有木有?!
 
视口:浏览器可见区域
pc端:视口可以调整
移动端:视口是固定的
 

5.应用场景
图片懒加载
github源码:Nirvana-zsy/lazyLoad
]]>
HashMap源码解析 http://doc.okbase.net/androidsuperman/archive/265410.html 西北野狼 2017/9/13 22:27:26

hashMap数据结构图:

HashMap特点:

 

  1. 允许一个记录的键为null;
  2. 允许多条记录的值为null;

  3. 非线程安全,任意时刻多线程操作hashmap,有可能导致数据不一致,可以通过Collections的synchronizedMap来实现Map的线程安全或者使用concurrentHashMap。

 

 

HashMap是链表+数组结构组成,底层是数组,数组元素是单向链表。当产生hash碰撞事件,意味着一个位置插入多个元素,这个时候数组上面就会产生链表。

通过hashcode的高16位实现的,能保证数组table的length比较小的时候,保证高低bit都参与到hash计算中,不会有大的开销。

static final int hash(Object key) {
        int h;
     // h = key.hashCode() 为第一步 取hashCode值
     // h ^ (h >>> 16)  为第二步 高位参与运算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

根据key的hash值进行value内容的查找

 public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

 

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

put实现:

对key的hashCode()进行hashing,并计算下标( n-1 & hash),判断该位置元素是否存在,不存在,创建Node元素,存在产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    //tab数组,p每个桶
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    //tab为空创建tab
        if ((tab = table) == null || (n = tab.length) == 0)
            //resize进行扩容
            n = (tab = resize()).length;
    //index = (n - 1) & hash 下表位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            //创建一个新的Node元素
            tab[i] = newNode(hash, key, value, null);
        else {
            //指定位置已经有元素,也就是说产生hash碰撞
            Node<K,V> e; K k;
            //判断节点是否存在,覆盖原来原来的value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                //判断是否是红黑树
                //是红黑树
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //不是红黑树,遍历链表准备插入
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //尾插法添加元素
                        p.next = newNode(hash, key, value, null);
                        //TREEIFY_THRESHOLD默认为8,大于8,转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //如果达到这个阈值转为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果节点key存在,则覆盖原来位置的key
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //检查e是否存在相应的key,如果存在就更新value,并且返回
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //判断hashmap是否需要resize扩容
        if (++size > threshold)
            resize();
        //留给子类LinkedHashMap来实现的
        afterNodeInsertion(evict);
        return null;
    }

resize实现:HashMap扩容实现:使用一个新的数组代替已有的容量小的数组。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;            //创建一个oldTab数组用于保存之前的数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;    //获取原来数组的长度
        int oldThr = threshold;                //原来数组扩容的临界值
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {    //如果原来的数组长度大于最大值(2^30)
                threshold = Integer.MAX_VALUE;    //扩容临界值提高到正无穷
                return oldTab;                    //返回原来的数组,也就是系统已经管不了了,随便你怎么玩吧
            }
            //else if((新数组newCap)长度乘2) < 最大值(2^30) && (原来的数组长度)>= 初始长度(2^4))
            //这个else if 中实际上就是咋判断新数组(此时刚创建还为空)和老数组的长度合法性,同时交代了,
            //我们扩容是以2^1为单位扩容的。下面的newThr(新数组的扩容临界值)一样,在原有临界值的基础上扩2^1
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0)
            newCap = oldThr;    //新数组的初始容量设置为老数组扩容的临界值
        else {               // 否则 oldThr == 0,零初始阈值表示使用默认值
            newCap = DEFAULT_INITIAL_CAPACITY;    //新数组初始容量设置为默认值
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);        //计算默认容量下的阈值
        }
        if (newThr == 0) {    //如果newThr == 0,说明为上面 else if (oldThr > 0)
        //的情况(其他两种情况都对newThr的值做了改变),此时newCap = oldThr;
            float ft = (float)newCap * loadFactor;    //ft为临时变量,用于判断阈值的合法性
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);        //计算新的阈值
        }
        threshold = newThr; //改变threshold值为新的阈值
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;    //改变table全局变量为,扩容后的newTable
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {    //遍历数组,将老数组(或者原来的桶)迁移到新的数组(新的桶)中
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {    //新建一个Node<K,V>类对象,用它来遍历整个数组
                    oldTab[j] = null;
                    if (e.next == null)
                                //将e也就是oldTab[j]放入newTab中e.hash & (newCap - 1)的位置,
                        newTab[e.hash & (newCap - 1)] = e;    //这个我们之前讲过,是一个取模操作
                    else if (e instanceof TreeNode)        //如果e已经是一个红黑树的元素,这个我们不展开讲
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { 
                        //命名两组对象
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

工作原理总结:

通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。

]]>
如何实现一个 Virtual DOM 及源码分析 http://doc.okbase.net/tugenhua0707/archive/265409.html 龙恩0707 2017/9/13 22:27:19

如何实现一个 Virtual DOM 及源码分析

Virtual DOM算法

    web页面有一个对应的DOM树,在传统开发页面时,每次页面需要被更新时,都需要手动操作DOM来进行更新,但是我们知道DOM操作对性能来说是非常不友好的,会影响页面的重排,从而影响页面的性能。因此在React和VUE2.0+引入了虚拟DOM的概念,他们的原理是:把真实的DOM树转换成javascript对象树,也就是虚拟DOM,每次数据需要被更新的时候,它会生成一个新的虚拟DOM,并且和上次生成的虚拟DOM进行对比,对发生变化的数据做批量更新。---(因为操作JS对象会更快,更简单,比操作DOM来说)。
我们知道web页面是由一个个HTML元素嵌套组合而成的,当我们使用javascript来描述这些元素的时候,这些元素可以简单的被表示成纯粹的JSON对象。

比如如下HTML代码:

<div id="container" class="container">
   <ul id="list">
     <li class="item">111</li>
     <li class="item">222</li>
     <li class="item">333</li>
   </ul>
   <button class="btn btn-blue"><em>提交</em></button>
</div>

上面是真实的DOM树结构,我们可以使用javascript中的json对象来表示的话,变成如下:

var element = {
      tagName: 'div',
      props: {   // DOM的属性
        id: 'container',
        class: 'container'
      },
      children: [
        {
          tagName: 'ul',
          props: {
            id: 'list'
          },
          children: [
            {tagName: 'li', props: {class: 'item'}, children: ['111']},
            {tagName: 'li', props: {class: 'item'}, children: ['222']},
            {tagName: 'li', props: {class: 'item'}, children: ['333']}
          ]
        },
        {
          tagName: 'button',
          props: {
            class: 'btn btn-blue'
          },
          children: [
            {
              tagName: 'em',
              children: ['提交']
            }
          ]
        }
      ]
   };

因此我们可以使用javascript对象表示DOM的信息和结构,当状态变更的时候,重新渲染这个javascript对象的结构,然后可以使用新渲染的对象树去和旧的树去对比,记录两颗树的差异,两颗树的差异就是我们需要对页面真正的DOM操作,然后把他们应用到真正的DOM树上,页面就得到更新。视图的整个结构确实全渲染了,但是最后操作DOM的时候,只变更不同的地方。
因此我们可以总结一下 Virtual DOM算法:
1. 用javascript对象结构来表示DOM树的结构,然后用这个树构建一个真正的DOM树,插入到文档中。
2. 当状态变更的时候,重新构造一颗新的对象树,然后使用新的对象树与旧的对象树进行对比,记录两颗树的差异。
3. 把记录下来的差异用到步骤1所构建的真正的DOM树上。视图就更新了。

算法实现:
2-1 使用javascript对象模拟DOM树。
使用javascript来表示一个DOM节点,有如上JSON的数据,我们只需要记录它的节点类型,属性和子节点即可。

element.js 代码如下:

function Element(tagName, props, children) {
  this.tagName = tagName;
  this.props = props;
  this.children = children;
}
Element.prototype.render = function() {
  var el = document.createElement(this.tagName);
  var props = this.props;
  // 遍历子节点,依次设置子节点的属性
  for (var propName in props) {
    var propValue = props[propName];
    el.setAttribute(propName, propValue);
  }
  // 保存子节点
  var childrens = this.children || [];
  // 遍历子节点,使用递归的方式 渲染
  childrens.forEach(function(child) {
    var childEl = (child instanceof Element) ? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
      : document.createTextNode(child);    // 如果是字符串的话,只构建文本节点
    el.appendChild(childEl);
  });
  return el;
};
module.exports = function(tagName, props, children) {
  return new Element(tagName, props, children);
}

入口index.js代码如下:

var el = require('./element');

var element = el('div', {id: 'container', class: 'container'}, [
  el('ul', {id: 'list'},[
    el('li', {class: 'item'}, ['111']),
    el('li', {class: 'item'}, ['222']),
    el('li', {class: 'item'}, ['333']),
  ]),
  el('button', {class: 'btn btn-blue'}, [
    el('em', {class: ''}, ['提交'])
  ])
]);

var elemRoot = element.render();
document.body.appendChild(elemRoot);

打开页面即可看到效果。

2-2 比较两颗虚拟DOM树的差异及差异的地方进行dom操作

上面的div只会和同一层级的div对比,第二层级的只会和第二层级的对比,这样的算法的复杂度可以达到O(n).
但是在实际代码中,会对新旧两颗树进行一个深度优先的遍历,因此每个节点都会有一个标记。如下图所示:

在遍历的过程中,每次遍历到一个节点就把该节点和新的树进行对比,如果有差异的话就记录到一个对象里面。

现在我们来看下我的目录下 有哪些文件;然后分别对每个文件代码进行解读,看看做了哪些事情,旧的虚拟dom和新的虚拟dom是如何比较的,且是如何更新页面的 如下目录:
目录结构如下:

vdom  ---- 工程名
|   | ---- index.html  html页面
|   | ---- element.js  实例化元素组成json数据 且 提供render方法 渲染页面
|   | ---- util.js     提供一些公用的方法
|   | ---- diff.js     比较新旧节点数据 如果有差异保存到一个对象里面去
|   | ---- patch.js    对当前差异的节点数据 进行DOM操作
|   | ---- index.js    页面代码初始化调用

首先是 index.js文件 页面渲染完成后 变成如下html结构 

<div id="container">
  <h1 style="color: red;">simple virtal dom</h1>
  <p>the count is :1</p>
  <ul>
    <li>Item #0</li>
  </ul>
</div>

假如发生改变后,变成如下结构 

<div id="container">
  <h1 style="color: blue;">simple virtal dom</h1>
  <p>the count is :2</p>
  <ul>
    <li>Item #0</li>
    <li>Item #1</li>
  </ul>
</div>

可以看到 新旧节点页面数据的改变,h1标签从属性 颜色从红色 变为蓝色,p标签的文本发生改变,ul新增了一项元素li。
基本的原理是:先渲染出页面数据出来,生成第一个模板页面,然后使用定时器会生成一个新的页面数据出来,对新旧两颗树进行一个深度优先的遍历,因此每个节点都会有一个标记。
然后调用diff方法对比对象新旧节点遍历进行对比,找出两者的不同的地方存入到一个对象里面去,最后通过patch.js找出对象不同的地方,分别进行dom操作。

index.js代码如下:

var el = require('./element');
var diff = require('./diff');
var patch = require('./patch');

var count = 0;
function renderTree() {
  count++;
  var items = [];
  var color = (count % 2 === 0) ? 'blue' : 'red';
  for (var i = 0; i < count; i++) {
    items.push(el('li', ['Item #' + i]));
  }
  return el('div', {'id': 'container'}, [
    el('h1', {style: 'color: ' + color}, ['simple virtal dom']),
    el('p', ['the count is :' + count]),
    el('ul', items)
  ]);
}

var tree = renderTree()
var root = tree.render()
document.body.appendChild(root)
setInterval(function () {
  var newTree = renderTree()
  var patches = diff(tree, newTree)
  console.log(patches)
  patch(root, patches)
  tree = newTree
}, 1000);

执行 var tree = renderTree()方法后,会调用element.js,
1. 依次遍历子节点(从内到外调用)依次为 li, h1, p, ul, li和h1和p有一个文本子节点,因此遍历完成后,count就等于1,
但是遍历ul的时候,因为有一个子节点li,因此 count += 1; 所以调用完成后,ul的count等于2. 因此会对每个element属性添加count属性。对于最外层的container容器就是对每个子节点的依次增加,h1子节点默认为1,循环完成后 +1;因此变为2, p节点默认为1,循环完成后 +1,因此也变为2,ul为2,循环完成后 +1,因此变为3,因此container节点的count=2+2+3 = 7;

element.js部分代码如下:

function Element(tagName, props, children) {
  if (!(this instanceof Element)) {
    // 判断子节点 children 是否为 undefined
    if (!utils.isArray(children) && children !== null) {
      children = utils.slice(arguments, 2).filter(utils.truthy);
    }
    return new Element(tagName, props, children);
  }
  // 如果没有属性的话,第二个参数是一个数组,说明第二个参数传的是子节点
  if (utils.isArray(props)) {
    children = props;
    props = {};
  }
  this.tagName = tagName;
  this.props = props || {};
  this.children = children || [];
  // 保存key键 如果有属性 保存key,否则返回undefined
  this.key = props ? props.key : void 0;
  var count = 0;
  
  utils.each(this.children, function(child, i) {
    // 如果是元素的实列的话
    if (child instanceof Element) {
      count += child.count;
    } else {
      // 如果是文本节点的话,直接赋值
      children[i] = '' + child;
    }
    count++;
  });
  this.count = count;
}

oldTree数据最终变成如下:

var oldTree = {
  tagName: 'div',
  key: undefined,
  count: 7,
  props: {id: 'container'},
  children: [
    {
      tagName: 'h1',
      key: undefined
      count: 1
      props: {style: 'colod: red'},
      children: ['simple virtal dom']
    },
    {
      tagName: 'p',
      key: undefined
      count: 1
      props: {},
      children: ['the count is :1']
    },
    {
      tagName: 'ul',
      key: undefined
      count: 2
      props: {},
      children: [
        {
          tagName: 'li',
          key: undefined,
          count: 1,
          props: {},
          children: ['Item #0']
        }
      ]
    },
  ]
};

定时器 执行 var newTree = renderTree()后,调用方法步骤还是和第一步一样:
2. 依次遍历子节点(从内到外调用)依次为 li, h1, p, ul, li和h1和p有一个文本子节点,因此遍历完成后,count就等于1,因为有2个子元素li,count都为1,因此ul每次遍历依次在原来的基础上加1,因此遍历完成第一个li时候,ul中的count为2,当遍历完成第二个li的时候,ul的count就为4了。因此ul中的count为4. 对于最外层的container容器就是对每个子元素依次增加。
所以 container节点的count = 2 + 2 + 5 = 9;

newTree数据最终变成如下数据:

var newTree = {
  tagName: 'div',
  key: undefined,
  count: 9,
  props: {id: 'container'},
  children: [
    {
      tagName: 'h1',
      key: undefined
      count: 1
      props: {style: 'colod: red'},
      children: ['simple virtal dom']
    },
    {
      tagName: 'p',
      key: undefined
      count: 1
      props: {},
      children: ['the count is :1']
    },
    {
      tagName: 'ul',
      key: undefined
      count: 4
      props: {},
      children: [
        {
          tagName: 'li',
          key: undefined,
          count: 1,
          props: {},
          children: ['Item #0']
        },
        {
          tagName: 'li',
          key: undefined,
          count: 1,
          props: {},
          children: ['Item #1']
        }
      ]
    },
  ]
}

var patches = diff(oldTree, newTree);

调用diff方法可以比较新旧两棵树节点的数据,把两颗树的不同节点找出来。(注意,查看diff对比数据的方法,找到不同的节点,可以查看这篇文章diff算法)如下调用代码:

function diff (oldTree, newTree) {
  var index = 0;
  var patches = {};
  deepWalk(oldTree, newTree, index, patches);
  return patches;
}

执行deepWalk如下代码:

function deepWalk(oldNode, newNode, index, patches) {
  var currentPatch = [];
  // 节点被删除掉
  if (newNode === null) {
    // 真正的DOM节点时,将删除执行重新排序,所以不需要做任何事
  } else if(utils.isString(oldNode) && utils.isString(newNode)) {
    // 替换文本节点
    if (newNode !== oldNode) {
      currentPatch.push({type: patch.TEXT, content: newNode});
    }
  } else if(oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
    // 相同的节点,但是新旧节点的属性不同的情况下 比较属性
    // diff props
    var propsPatches = diffProps(oldNode, newNode);
    if (propsPatches) {
      currentPatch.push({type: patch.PROPS, props: propsPatches});
    }
    // 不同的子节点 
    if (!isIgnoreChildren(newNode)) {
      diffChildren(
        oldNode.children,
        newNode.children,
        index,
        patches,
        currentPatch
      )
    }
  } else {
    // 不同的节点,那么新节点替换旧节点
    currentPatch.push({type: patch.REPLACE, node: newNode});
  }
  if (currentPatch.length) {
    patches[index] = currentPatch;
  }
}

1. 判断新节点是否为null,如果为null,说明节点被删除掉。
2. 判断新旧节点是否为字符串,如果为字符串说明是文本节点,并且新旧两个文本节点不同的话,存入数组里面去,如下代码:

   currentPatch.push({type: patch.TEXT, content: newNode});
   patch.TEXT 为 patch.js里面的 TEXT = 3;content属性为新节点。

3. 如果新旧tagName相同的话,并且新旧节点的key相同的话,继续比较新旧节点的属性,如下代码:

var propsPatches = diffProps(oldNode, newNode);

diffProps方法的代码如下:

function diffProps(oldNode, newNode) {
      var count = 0;
      var oldProps = oldNode.props;
      var newProps = newNode.props;
      var key,
        value;
      var propsPatches = {};
      // 找出不同的属性值
      for (key in oldProps) {
        value = oldProps[key];
        if (newProps[key] !== value) {
          count++;
          propsPatches[key] = newProps[key];
        }
      }
      // 找出新增属性
      for (key in newProps) {
        value = newProps[key];
        if (!oldProps.hasOwnProperty(key)) {
          count++;
          propsPatches[key] = newProps[key];
        }
      }
      // 如果所有的属性都是相同的话
      if (count === 0) {
        return null;
      }
      return propsPatches;
   }

diffProps代码解析如下:

for (key in oldProps) {
   value = oldProps[key];
   if (newProps[key] !== value) {
      count++;
      propsPatches[key] = newProps[key];
   }
}

如上代码是 判断旧节点的属性值是否在新节点中找到,如果找不到的话,count++; 把新节点的属性值赋值给 propsPatches 存储起来。

for (key in newProps) {
   value = newProps[key];
   if (!oldProps.hasOwnProperty(key)) {
      count++;
      propsPatches[key] = newProps[key];
   }
}

如上代码是 判断新节点的属性是否能在旧节点中找到,如果找不到的话,count++; 把新节点的属性值赋值给 propsPatches 存储起来。

if (count === 0) {
   return null;
}
return propsPatches;

最后如果count 等于0的话,说明所有属性都是相同的话,所以不需要做任何变化。否则的话,返回新增的属性。

如果有 propsPatches 的话,执行如下代码:

if (propsPatches) {
   currentPatch.push({type: patch.PROPS, props: propsPatches});
}

因此currentPatch数组里面也有对应的更新的属性,props就是需要更新的属性对象。

继续代码:

// 不同的子节点 
if (!isIgnoreChildren(newNode)) {
   diffChildren(
     oldNode.children,
     newNode.children,
     index,
     patches,
     currentPatch
   )
}
function isIgnoreChildren(node) {
  return (node.props && node.props.hasOwnProperty('ignore'));
}

如上代码判断子节点是否相同,diffChildren代码如下:

function diffChildren(oldChildren, newChildren, index, patches, currentPatch) {
    var diffs = listDiff(oldChildren, newChildren, 'key');
    newChildren = diffs.children;

    if (diffs.moves.length) {
      var recorderPatch = {type: patch.REORDER, moves: diffs.moves};
      currentPatch.push(recorderPatch);
    }

    var leftNode = null;
    var currentNodeIndex = index;
    utils.each(oldChildren, function(child, i) {
      var newChild = newChildren[i];
      currentNodeIndex = (leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1;
      // 递归
      deepWalk(child, newChild, currentNodeIndex, patches);
      leftNode = child;
    });
  }

如上代码:var diffs = listDiff(oldChildren, newChildren, 'key'); 新旧节点按照key来比较,目前key为undefined,所以diffs 为如下:

diffs = {
    moves: [],
    children: [
      {
        tagName: 'h1',
        key: undefined
        count: 1
        props: {style: 'colod: blue'},
        children: ['simple virtal dom']
      },
      {
        tagName: 'p',
        key: undefined
        count: 1
        props: {},
        children: ['the count is :2']
      },
      {
        tagName: 'ul',
        key: undefined
        count: 4
        props: {},
        children: [
          {
            tagName: 'li',
            key: undefined,
            count: 1,
            props: {},
            children: ['Item #0']
          },
          {
            tagName: 'li',
            key: undefined,
            count: 1,
            props: {},
            children: ['Item #1']
          }
        ]
      }
    ]
  };

newChildren = diffs.children;
oldChildren数据如下:

oldChildren = [
    {
      tagName: 'h1',
      key: undefined
      count: 1
      props: {style: 'colod: red'},
      children: ['simple virtal dom']
    },
    {
      tagName: 'p',
      key: undefined
      count: 1
      props: {},
      children: ['the count is :1']
    },
    {
      tagName: 'ul',
      key: undefined
      count: 2
      props: {},
      children: [
        {
          tagName: 'li',
          key: undefined,
          count: 1,
          props: {},
          children: ['Item #0']
        }
      ]
    }
  ];

接着就是遍历 oldChildren, 第一次遍历时 leftNode 为null,因此 currentNodeIndex = currentNodeIndex + 1 = 0 + 1 = 1; 不是第一次遍历,那么leftNode都为上一次遍历的子节点,因此不是第一次遍历的话,那么 currentNodeIndex = currentNodeIndex + leftNode.count + 1; 
然后递归调用 deepWalk(child, newChild, currentNodeIndex, patches); 方法,接着把child赋值给leftNode,leftNode = child;

所以一直递归遍历,最终把不相同的节点 会存储到 currentPatch 数组内。最后执行

if (currentPatch.length) {
   patches[index] = currentPatch;
}

把对应的currentPatch 存储到 patches对象内中的对应项,最后就返回 patches对象。

4. 返回到index.js 代码内,把两颗不相同的树节点的提取出来后,需要调用patch.js方法传进;把不相同的节点应用到真正的DOM上.
不相同的节点 patches数据如下:

patches = {
    1: [{type: 2, props: {style: 'color: blue'}}],
    4: [{type: 3, content: 'the count is :2'}],
    5: [
          { 
              type: 1, 
              moves: [
                { index: 1, 
                   item: {
                      tagName: 'li',
                      props: {},
                      count: 1,
                      key: undefined,
                      children: ['Item #1']
                    }
                }
              ]
           }
        ]
    }

如下代码调用:
patch(root, patches);
执行patch方法,代码如下:

function patch(node, patches) {
  var walker = {index: 0};
  deepWalk(node, walker, patches);
}

deepWalk 代码如下:

function deepWalk(node, walker, patches) {
   var currentPatches = patches[walker.index];
      // node.childNodes 返回指定元素的子元素集合,包括HTML节点,所有属性,文本节点。
   var len = node.childNodes ? node.childNodes.length : 0;
   for (var i = 0; i < len; i++) {
      var child = node.childNodes[i];
      walker.index++;
      // 深度复制 递归遍历
      deepWalk(child, walker, patches);
   }
   if (currentPatches) {
      applyPatches(node, currentPatches);
   }
}

1. 首次调用patch的方法,root就是container的节点,因此调用deepWalk方法,因此 var currentPatches = patches[0] = undefined,
var len = node.childNodes ? node.childNodes.length : 0; 因此 len = 3; 很明显该子节点的长度为3,因为子节点有 h1, p, 和ul元素;


2. 然后进行for循环,获取该父节点的子节点,因此第一个子节点为 h1 元素,walker.index++; 因此walker.index = 1; 再进行递归 deepWalk(child, walker, patches); 此时子节点为h1, walker.index为1, 因此获取 currentPatches = patches[1]; 获取值,再获取 h1的子节点的长度,len = 1; 然后再for循环,获取child为文本节点,此时 walker.index++; 所以此时walker.index 为2, 在调用deepwalk方法递归,因此再继续获取 currentPatches = patches[2]; 值为undefined,再获取len = 0; 因为文本节点么有子节点,所以for循环跳出,所以判断currentPatches是否有值,因为此时 currentPatches 为undefined,所以递归结束,再返回到 h1元素上来,所以currentPatches = patches[1]; 所以有值,所以调用 applyPatches()方法来更新dom元素。


3. 继续循环 i, 此时i = 1; 获取子节点 child = p元素,walker.index++,此时walker.index = 3, 继续调用 deepWalk方法,获取 var currentPatches = patches[walker.index] = patches[3]的值,var len = 1; 因为p元素下有一个子节点(文本节点),再进for循环,此时 walker.index++; 因此walker.index = 4; child此时为文本节点,在调用 deepwalk方法的时候,再获取var currentPatches = patches[walker.index] = patches[4]; 再执行len 代码的时候 len = 0;因此跳出for循环,判断 currentPatches是否有值,有值的话,更新对应的DOM元素。

4. 继续循环i = 2; 获取子节点 child = ul元素,walker.index++; 此时walker.index = 5; 在调用deepWalk方法递归,因此再获取 var currentPatches = patches[walker.index] = patches[5]; 然后len = 1, 因为ul元素下有一个li元素,在继续for循环遍历,获取子节点li,此时walker.index++; walker.index = 6; 再递归调用deepwalk方法,再获取var currentPatches = patches[walker.index] = patches[6]; len = 1; 因为li的元素下有一个文本节点,再进行for循环,此时child为文本节点,walker.index++;此时walker.index = 7; 再执行 deepwalk方法,再获取 var currentPatches = patches[walker.index] = patches[7]; 这时候 len = 0了,因此跳出for循环,判断 当前的currentPatches是否有值,没有,就跳出,然后再返回ul元素,获取该自己li的时候,walker.index 等于5,因此var currentPatches = patches[walker.index] = patches[5]; 然后判断 currentPatches是否有值,有值就进行更新DOM元素。

最后就是 applyPatches 方法更新dom元素了,如下代码:

function applyPatches(node, currentPatches) {
  utils.each(currentPatches, function(currentPatch) {
    switch (currentPatch.type) {
      case REPLACE:
        var newNode = (typeof currentPatch.node === 'string') 
          ? document.createTextNode(currentPatch.node)
          : currentPatch.node.render();
        node.parentNode.replaceChild(newNode, node);
        break;
      case REORDER:
        reorderChildren(node, currentPatch.moves);
        break;
      case PROPS: 
        setProps(node, currentPatch.props);
        break;
      case TEXT:
        if(node.textContent) {
          node.textContent = currentPatch.content;
        } else {
          // ie bug
          node.nodeValue = currentPatch.content;
        }
        break;
      default:
        throw new Error('Unknow patch type' + currentPatch.type);
    }
  });
}

判断类型,替换对应的属性和节点。
最后就是对子节点进行排序的操作,代码如下:

// 对子节点进行排序
function reorderChildren(node, moves) {
  var staticNodeList = utils.toArray(node.childNodes);
  var maps = {};
  utils.each(staticNodeList, function(node) {
    // 如果是元素节点
    if (node.nodeType === 1) {
      var key = node.getAttribute('key');
      if (key) {
        maps[key] = node;
      }
    }
  })
  utils.each(moves, function(move) {
    var index = move.index;
    if (move.type === 0) {
      // remove Item
      if (staticNodeList[index] === node.childNodes[index]) {
        node.removeChild(node.childNodes[index]);
      }
      staticNodeList.splice(index, 1);
    } else if(move.type === 1) {
      // insert item
      var insertNode = maps[move.item.key] 
        ? maps[move.item.key].cloneNode(true)
        : (typeof move.item === 'object') ? move.item.render() : document.createTextNode(move.item);
      staticNodeList.splice(index, 0, insertNode);
      node.insertBefore(insertNode, node.childNodes[index] || null);
    }
  });
}

遍历moves,判断moves.type 是等于0还是等于1,等于0的话是删除操作,等于1的话是新增操作。比如现在moves值变成如下:

moves = {
  index: 1,
  type: 1,
  item: {
    tagName: 'li',
    key: undefined,
    props: {},
    count: 1,
    children: ['#Item 1']
  }
};

node节点 就是 'ul'元素,var staticNodeList = utils.toArray(node.childNodes); 把ul的旧子节点li转成Array形式,由于没有属性key,所以直接跳到下面遍历代码来,遍历moves,获取某一项的索引index,判断move.type 等于0 还是等于1, 目前等于1,是新增一项,但是没有key,因此调用move.item.render(); 渲染完后,对staticNodeList数组里面的旧节点的li项从第二项开始插入节点li,然后执行node.insertBefore(insertNode, node.childNodes[index] || null); node就是ul父节点,insertNode节点插入到 node.childNodes[1]的前面。因此把在第二项的前面插入第一项。
查看github上源码

]]>
Centos 6,7安装用yum命令 mysql 5.7 http://doc.okbase.net/zdhsoft/archive/265408.html zdhsoft 2017/9/8 8:09:14

1.Yum包的官方地址:

https://dev.mysql.com/downloads/repo/yum/


选择对应系统的rpm包下载 下载的时候要登录的时候,请注册一个。

 然后,把对应的rpm包下载下来

 

2:然后把rpm包,传到Linux系统,centos 6,7是有区分的,请对系统做相应的操作。

Centos7 :mysql57-community-release-el7-11.noarch.rpm

Centos6:mysql57-community-release-el6-11.noarch.rpm

 

3:解压安装rpm

输入su,进入root模式

输入:rpm -Uvh mysql57-community-release-el7-11.noarch.rpm安装对应的yum

输入:yum repolist all | grep MySQL

 

 

4.安装

安装mysql:yum install mysql-community-server

5.启动

启动mysql:service mysqld start

6.设置密码

注意一下:数据库初始化,必须要重置密码才能使用,也就是使用alter user命令将root密码重置。

 

输入:grep'temporarypassword' /var/log/mysqld.log查看密码

然后输入:mysql-uroot -p连接本地的mysql,提示输入的密码,就是那个上面grep命令显示的

 

进入mysql后,用下面的命令修改密码:

mysql>ALTER USER'root'@'localhost'IDENTIFIED BY'MyNewPass4!';

 

 

 

安装完成后,安装的数据在:/var/lib/mysql

 

1、关闭firewall:

systemctl stop firewalld.service #停止firewall

systemctl disable firewalld.service #禁止firewall开机启动

firewall-cmd--state #查看默认防火墙状态(关闭后显示notrunning,开启后显示running)

 

 

//配置连接方式和权限,注意,要执行flush privileges;否则会连接不了

grantall on *.* to rock@'%' identified by 'NewPassword1' with grant option;

flush privileges;

]]>
StdTranslator - Translate PDMS to STD for STAAD.Pro http://doc.okbase.net/eryar/archive/265407.html eryar 2017/9/8 8:08:59

StdTranslator - Translate PDMS to STD for STAAD.Pro

eryar@163.com

STAAD.Pro是由美国世界著名的工程咨询和CAD软件开发公司—REI(Research Engineering International)从上世纪七十年代开始开发的通用有限元结构分析与设计软件,到2005年底统计,在全球近百个国家中已超过160,000用户。

STAAD.Pro本身具有强大的三维建模系统及丰富的结构模板,用户可方便快捷地直接建立各种复杂三维模型。用户亦可通过导入其他软件(例如AUTOCAD)生成的标准DXF文件在STAAD中生成模型。对各种异形空间曲线、二次曲面,用户可借助EXCEL电子表格生成模型数据后直接导入到STAAD中建模。最新的STAAD版本允许用户通过STAAD的数据接口运行用户自编宏模。高级用户可用各种方式编辑STAAD的核心的STD文件(纯文本文件)建模。用户可在设计的任何阶段对模型的部分或整体进行任意的移动、旋转、复制、镜象、阵列等操作。

StdTranslator程序是在PDMS中开发的,主要用于将PDMS结构模型导出为STAAD.Pro的STD文件。PDMS中的结构模型对于结构分析来说需要重新建模,所以结构专业也不愿意进入PDMS进行协同设计。有了StdTranslator可以使PDMS中的结构可以快速的导入到STAAD.Pro中进行分析计算,不需要二次建模。

StdTranslator界面如下图所示:

PDMS中的结构模型如下图所示:

StdTranslator导出到STAAD.Pro中的模型如下图所示:

有了StdTranslator工具,可以帮助同时使用PDMS的管道专业和使用STAAD.Pro的结构专业更好地协同设计,提高效率。

]]>
Spring(20)——@PropertySource http://doc.okbase.net/234390216/archive/265406.html 234390216 2017/9/8 8:06:54

20 @PropertySource

在之前介绍<context:property-placeholder/>时提到过其默认会使用PropertySourcesPlaceholderConfigurer来进行对应的属性替换,其底层有使用PropertySource。@PropertySource是用来注册一个PropertySource的。PropertySource是用来表示一个name/value属性配对的资源的,可以简单的把它理解为我们熟悉的Properties。

Environment可以持有一系列的PropertySource,然后在从中获取属性时,其会依次从对应的PropertySource中寻找,当然也包括系统属性和环境变量。一个Environment中默认会包含两个PropertySource,分别对应于系统属性和环境变量。即默认情况下在只有系统属性和环境变量对应的两个ProperySource时,如果我们从Environment中获取某属性,将先从系统属性中取,没取到再从环境变量中获取。所以,如下示例我们直接从Environment中获取属性“user.dir”的值,这里取的就是系统属性“user.dir”的值。

	@Test
	public void testPropertySource() {
		ConfigurableApplicationContext context = new GenericApplicationContext();
		ConfigurableEnvironment env = context.getEnvironment();
		String userDir = env.getProperty("user.dir");
		System.out.println(userDir);
		context.close();
	}

我们也可以往Environment中添加PropertySource对象,之后添加的PropertySource对象就可以用来获取对应的属性。如下示例中我们往Environment中添加了一个基于类路径下的init.properties文件的ResourcePropertySource,且是加在所有的PropertySource之前,由于在从Environment中获取属性时,将优先从前面的PropertySource中获取。那么如果我们在init.properies文件中定义了一个user.dir属性,则下面示例中获取到的user.dir就将是我们在init.properties文件中指定的那个。

	@Test
	public void testPropertySource() throws IOException {
		ConfigurableApplicationContext context = new GenericApplicationContext();
		//获取Environment对象
		ConfigurableEnvironment env = context.getEnvironment();
		//获取Environment的PropertySources
		MutablePropertySources propertySources = env.getPropertySources();
		//new一个基于Resource的PropertySource
		ResourcePropertySource rps = new ResourcePropertySource(new ClassPathResource("init.properties"));
		//给当前Environment对象env添加一个PropertySource对象
		propertySources.addFirst(rps);
		String userDir = env.getProperty("user.dir");
		System.out.println(userDir);
		context.close();
	}

接下来我们来介绍一下@PropertySource。@PropertySource需要和@Configuration一起使用。其可以让我们非常方便的把外部资源定义封装成一个PropertySource对象添加到对应的Environment中。如下示例中我们通过在使用@Configuration进行标注的配置类上通过@PropertySource标注引入了一个类路径下的init.properties文件作为一个PropertySource添加到了当前的Environment中(当指定的资源未指定前缀时默认就会当做ClassPathResource处理)。之后我们在创建bean时就可以使用Environment对象来获取其中拥有的PropertySource中定义的属性对应的属性值,如下述示例中我们在创建hello时就从Environment中获取了属性“hello.name”的值。

@Configuration
@PropertySource("init.properties")
public class SpringConfig {

	@Autowired
	private Environment env;
	
	@Bean
	public Hello hello() {
		String helloName = env.getProperty("hello.name");
		Hello hello = new Hello(helloName);
		return hello;
	}
	
}

如果需要同时将多个外部文件作为PropertySource添加到对应的Environment中,则我们可以通过@PropertySource的value属性指定多个资源,其对应于一个数组。如下示例中我们就同时将类路径下的init.properties和init2.properties文件作为PropertySource添加到当前的Environment中。

@Configuration
@PropertySource({"init.properties", "init2.properties"})
public class SpringConfig {
	
}

我们也可以通过@PropertySources注解来定义多个@PropertySource,以添加多个PropertySource到Environment中。

@Configuration
@PropertySources({@PropertySource("init.properties"), @PropertySource("init2.properties")})
public class SpringConfig {

}

默认情况下我们通过@PropertySource指定的资源是必须存在的,否则Spring将抛出异常,当然我们也可以通过@PropertySource的ignoreResourceNotFound属性来指定是否忽略资源未找到的情况,默认为false,表示不忽略。如下示例中我们就通过ignoreResourceNotFound指定了忽略init.properties文件不存在的情况。

@Configuration
@PropertySource(value="init.properties", ignoreResourceNotFound=true)
public class SpringConfig {

}

(注:本文是基于Spring4.1.0所写)

]]>