Angularjs 源码分析2

2020-12-13 05:57

阅读:526

标签:des   blog   class   c   code   tar   

本文主要分析RootScopeProvider和ParseProvider

RootScopeProvider简介

今天这个rootscope可是angularjs里面比较活跃的一个provider,大家可以理解为一个模型M或者VM,它主要负责与控制器或者指令进行数据交互. 今天使用的源码跟上次分析的一样也是1.2.X系列,只不过这次用的是未压缩合并版的,方便大家阅读,可以在这里下载

从$get属性说起

说起这个$get属性,是每个系统provider都有的,主要是先保存要实例化的函数体,等待instanceinjector.invoke的时候来调用,因为$get的代码比较多,所以先上要讲的那部分,大家可以注意到了,在$get上面有一个digestTtl方法

?
1
2
3
4
5
6
this.digestTtl = function(value) {
    if (arguments.length) {
      TTL = value;
    }
    return TTL;
  };

这个是用来修改系统默认的dirty check次数的,默认是10次,通过在config里引用rootscopeprovider,可以调用这个方法传递不同的值来修改ttl(short for Time To Live)

下面来看下$get中的scope构造函数

?
1
2
3
4
5
6
7
8
9
10
11
12
13
function Scope() {
    this.$id = nextUid();
    this.$$phase = this.$parent = this.$$watchers =
                     this.$$nextSibling = this.$$prevSibling =
                     this.$$childHead = this.$$childTail = null;
    this[‘this‘] = this.$root =  this;
    this.$$destroyed = false;
    this.$$asyncQueue = [];
    this.$$postDigestQueue = [];
    this.$$listeners = {};
    this.$$listenerCount = {};
    this.$$isolateBindings = {};
}

可以看到在构造函数里定义了很多属性,我们来一一说明一下

  • $id, 通过nextUid方法来生成一个唯一的标识
  • $$phase, 这是一个状态标识,一般在dirty check时用到,表明现在在哪个阶段
  • $parent, 代表自己的上级scope属性
  • $$watchers, 保存scope变量当前所有的监控数据,是一个数组
  • $$nextSibling, 下一个兄弟scope属性
  • $$prevSibling, 前一个兄弟scope属性
  • $$childHead, 第一个子级scope属性
  • $$childTail, 最后一个子级scope属性
  • $$destroyed, 表示是否被销毁
  • $$asyncQueue, 代表异步操作的数组
  • $$postDigestQueue, 代表一个在dirty check之后执行的数组
  • $$listeners, 代表scope变量当前所有的监听数据,是一个数组
  • $$listenerCount, 暂无
  • $$isolateBindings, 暂无

通过这段代码,可以看出,系统默认会创建根作用域,并作为$rootScopeprovider实例返回.

?
1
2
3
var $rootScope = new Scope();
 
return $rootScope;

创建子级作用域是通过$new方法,我们来看看.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$new: function(isolate) {
        var ChildScope,
            child;
 
        if (isolate) {
          child = new Scope();
          child.$root = this.$root;
          // ensure that there is just one async queue per $rootScope and its children
          child.$$asyncQueue = this.$$asyncQueue;
          child.$$postDigestQueue = this.$$postDigestQueue;
        } else {
          // Only create a child scope class if somebody asks for one,
          // but cache it to allow the VM to optimize lookups.
          if (!this.$$childScopeClass) {
            this.$$childScopeClass = function() {
              this.$$watchers = this.$$nextSibling =
                  this.$$childHead = this.$$childTail = null;
              this.$$listeners = {};
              this.$$listenerCount = {};
              this.$id = nextUid();
              this.$$childScopeClass = null;
            };
            this.$$childScopeClass.prototype = this;
          }
          child = new this.$$childScopeClass();
        }
        child[‘this‘] = child;
        child.$parent = this;
        child.$$prevSibling = this.$$childTail;
        if (this.$$childHead) {
          this.$$childTail.$$nextSibling = child;
          this.$$childTail = child;
        } else {
          this.$$childHead = this.$$childTail = child;
        }
        return child;
      }

通过分析上面的代码,可以得出

  • isolate标识来创建独立作用域,这个在创建指令,并且scope属性定义的情况下,会触发这种情况,还有几种别的特殊情况,假如是独立作用域的话,会多一个$root属性,这个默认是指向rootscope的

  • 如果不是独立的作用域,则会生成一个内部的构造函数,把此构造函数的prototype指向当前scope实例

  • 通用的操作就是,设置当前作用域的$$childTail,$$childTail.$$nextSibling,$$childHead,this.$$childTail为生成的子级作用域;设置子级域的$parent为当前作用域,$$prevSibling为当前作用域最后一个子级作用域

说完了创建作用域,再来说说$watch函数,这个比较关键

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$watch: function(watchExp, listener, objectEquality) {
        var scope = this,
            get = compileToFn(watchExp, ‘watch‘),
            array = scope.$$watchers,
            watcher = {
              fn: listener,
              last: initWatchVal,
              get: get,
              exp: watchExp,
              eq: !!objectEquality
            };
 
        lastDirtyWatch = null;
 
        // in the case user pass string, we need to compile it, do we really need this ?
        if (!isFunction(listener)) {
          var listenFn = compileToFn(listener || noop, ‘listener‘);
          watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);};
        }
 
        if (typeof watchExp == ‘string‘ && get.constant) {
          var originalFn = watcher.fn;
          watcher.fn = function(newVal, oldVal, scope) {
            originalFn.call(this, newVal, oldVal, scope);
            arrayRemove(array, watcher);
          };
        }
 
        if (!array) {
          array = scope.$$watchers = [];
        }
        // we use unshift since we use a while loop in $digest for speed.
        // the while loop reads in reverse order.
        array.unshift(watcher);
 
        return function deregisterWatch() {
          arrayRemove(array, watcher);
          lastDirtyWatch = null;
        };
      }

$watch函数有三个参数,第一个是监控参数,可以是字符串或者函数,第二个是监听函数,第三个是代表是否深度监听,注意看这个代码

?
1
get = compileToFn(watchExp, ‘watch‘)

这个compileToFn函数其实是调用$parse实例来分析监控参数,然后返回一个函数,这个会在dirty check里用到,用来获取监控表达式的值,这个$parseprovider也是angularjs中用的比较多的,下面来重点的说下这个provider

$parse的代码比较长,在源码文件夹中的ng目录里,parse.js里就是$parse的全部代码,当你了解完parse的核心之后,这部份代码其实可以独立出来,做成自己的计算器程序也是可以的,因为它的核心就是解析字符串,而且默认支持四则运算,运算符号的优先级处理,只是额外的增加了对变量的支持以及过滤器的支持,想想,把这块代码放在模板引擎里也是可以的,说多了,让我们来一步一步的分析parse代码吧.

记住,不管是哪个provider,先看它的$get属性,所以我们先来看看$parse的$get吧

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
this.$get = [‘$filter‘, ‘$sniffer‘, ‘$log‘, function($filter, $sniffer, $log) {
    $parseOptions.csp = $sniffer.csp;
 
    promiseWarning = function promiseWarningFn(fullExp) {
      if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return;
      promiseWarningCache[fullExp] = true;
      $log.warn(‘[$parse] Promise found in the expression ‘ + fullExp + ‘. ‘ +
          ‘Automatic unwrapping of promises in Angular expressions is deprecated.‘);
    };
 
    return function(exp) {
      var parsedExpression;
 
      switch (typeof exp) {
        case ‘string‘:
 
          if (cache.hasOwnProperty(exp)) {
            return cache[exp];
          }
 
          var lexer = new Lexer($parseOptions);
          var parser = new Parser(lexer, $filter, $parseOptions);
          parsedExpression = parser.parse(exp, false);
 
          if (exp !== ‘hasOwnProperty‘) {
            // Only cache the value if it‘s not going to mess up the cache object
            // This is more performant that using Object.prototype.hasOwnProperty.call
            cache[exp] = parsedExpression;
          }
 
          return parsedExpression;
 
        case function‘:
          return exp;
 
        default:
          return noop;
      }
    };
  }];

可以看出,假如解析的是函数,则直接返回,是字符串的话,则需要进行parser.parse方法,这里重点说下这个

通过阅读parse.js文件,你会发现,这里有两个关键类

  • lexer, 负责解析字符串,然后生成token,有点类似编译原理中的词法分析器

  • parser, 负责对lexer生成的token,生成执行表达式,其实就是返回一个执行函数

看这里

?
1
2
3
var lexer = new Lexer($parseOptions);
var parser = new Parser(lexer, $filter, $parseOptions);
parsedExpression = parser.parse(exp, false);

第一句就是创建一个lexer实例,第二句是把lexer实例传给parser构造函数,然后生成parser实例,最后一句是调用parser.parse生成执行表达式,实质是一个函数

现在转到parser.parse里去

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
parse: function (text, json) {
    this.text = text;
 
    //TODO(i): strip all the obsolte json stuff from this file
    this.json = json;
 
    this.tokens = this.lexer.lex(text);
 
    console.log(this.tokens);
 
    if (json) {
      // The extra level of aliasing is here, just in case the lexer misses something, so that
      // we prevent any accidental execution in JSON.
      this.assignment = this.logicalOR;
 
      this.functionCall =
      this.fieldAccess =
      this.objectIndex =
      this.filterChain = function() {
        this.throwError(‘is not valid json‘, {text: text, index: 0});
      };
    }
 
    var value = json ? this.primary() : this.statements();
 
    if (this.tokens.length !== 0) {
      this.throwError(‘is an unexpected token‘, this.tokens[0]);
    }
 
    value.literal = !!value.literal;
    value.constant = !!value.constant;
 
    return value;
  }

视线移到这句this.tokens = this.lexer.lex(text),然后来看看lex方法

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
lex: function (text) {
    this.text = text;
 
    this.index = 0;
    this.ch = undefined;
    this.lastCh = ‘:‘; // can start regexp
 
    this.tokens = [];
 
    var token;
    var json = [];
 
    while (this.index this.text.length) {
      this.ch = this.text.charAt(this.index);
      if (this.is(‘"\‘‘)) {
        this.readString(this.ch);
      } else if (this.isNumber(this.ch) || this.is(‘.‘) && this.isNumber(this.peek())) {
        this.readNumber();
      } else if (this.isIdent(this.ch)) {
        this.readIdent();
        // identifiers can only be if the preceding char was a { or ,
        if (this.was(‘{,‘) && json[0] === ‘{‘ &&
            (token = this.tokens[this.tokens.length - 1])) {
          token.json = token.text.indexOf(‘.‘) === -1;
        }
      } else if (this.is(‘(){}[].,;:?‘)) {
        this.tokens.push({
          index: this.index,
          text: this.ch,
          json: (this.was(‘:[,‘) && this.is(‘{[‘)) || this.is(‘}]:,‘)
        });
        if (this.is(‘{[‘)) json.unshift(this.ch);
        if (this.is(‘}]‘)) json.shift();
        this.index++;
      } else if (this.isWhitespace(this.ch)) {
        this.index++;
        continue;
      } else {
        var ch2 = this.ch + this.peek();
        var ch3 = ch2 + this.peek(2);
        var fn = OPERATORS[this.ch];
        var fn2 = OPERATORS[ch2];
        var fn3 = OPERATORS[ch3];
        if (fn3) {
          this.tokens.push({index: this.index, text: ch3, fn: fn3});
          this.index += 3;
        } else if (fn2) {
          this.tokens.push({index: this.index, text: ch2, fn: fn2});
          this.index += 2;
        } else if (fn) {
          this.tokens.push({
            index: this.index,
            text: this.ch,
            fn: fn,
            json: (this.was(‘[,:‘) && this.is(‘+-‘))
          });
          this.index += 1;
        } else {
          this.throwError(‘Unexpected next character ‘, this.index, this.index + 1);
        }
      }
      this.lastCh = this.ch;
    }
    return this.tokens;
  }

这里我们假如传进的字符串是1+2,通常我们分析源码的时候,碰到代码复杂的地方,我们可以简单化处理,因为逻辑都一样,只是情况不一样罢了.

上面的代码主要就是分析传入到lex内的字符串,以一个whileloop开始,然后依次检查当前字符是否是数字,是否是变量标识等,假如是数字的话,则转到 readNumber方法,这里以1+2为例,当前ch是1,然后跳到readNumber方法

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
readNumber: function() {
    var number = ‘‘;
    var start = this.index;
    while (this.index this.text.length) {
      var ch = lowercase(this.text.charAt(this.index));
      if (ch == ‘.‘ || this.isNumber(ch)) {
        number += ch;
      } else {
        var peekCh = this.peek();
        if (ch == ‘e‘ && this.isExpOperator(peekCh)) {
          number += ch;
        } else if (this.isExpOperator(ch) &&
            peekCh && this.isNumber(peekCh) &&
            number.charAt(number.length - 1) == ‘e‘) {
          number += ch;
        } else if (this.isExpOperator(ch) &&
            (!peekCh || !this.isNumber(peekCh)) &&
            number.charAt(number.length - 1) == ‘e‘) {
          this.throwError(‘Invalid exponent‘);
        } else {
          break;
        }
      }
      this.index++;
    }
    number = 1 * number;
    this.tokens.push({
      index: start,
      text: number,
      json: true,
      fn: function() { return number; }
    });
  }

上面的代码就是检查从当前index开始的整个数字,包括带小数点的情况,检查完毕之后跳出loop,当前index向前进一个,以待以后检查后续字符串,最后保存到lex实例的token数组中,这里的fn属性就是以后执行时用到的,这里的return number是利用了JS的闭包特性,number其实就是检查时外层的number变量值.以1+2为例,这时index应该停在+这里,在lex的while loop中,+检查会跳到最后一个else里,这里有一个对象比较关键,OPERATORS,它保存着所有运算符所对应的动作,比如这里的+,对应的动作是

?
&lt
1
2
3
4
5
6
7
8
9
‘+‘: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;}


评论


亲,登录后才可以留言!