您现在的位置:首页 >> 前端 >> 内容

AngularJSRootScope源码分析

时间:2016/8/12 10:10:33 点击:

  核心提示:AngularJS RootScope 源码分析预备知识:Provider 中的$get 属性说起这个$get属性,是每个系统provider都有的,主要是先保存要实例化的函数体,等待instance...

AngularJS RootScope 源码分析

预备知识:Provider 中的$get 属性

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

this.digestTtl = function(value) {
    if (arguments.length) {
      TTL = value;
    }
    return TTL;
  };

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

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

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实例返回.

    var $rootScope = new Scope();
    
    return $rootScope;
    

    创建子级作用域是通过$new方法

    $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

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

      get = compileToFn(watchExp, 'watch')
      

      这个compileToFn函数其实是调用$parse实例来分析监控参数,然后返回一个函数,这个会在dirty check里用到,用来获取监控表达式的值。同时这里的get = compileToFn(watchExp, 'watch')返回的get是一个可执行的表达式函数

      $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;
              };
            }

      接着:

      watcher = {
                    fn: listener,
                    last: initWatchVal,
                    get: get,
                    exp: watchExp,
                    eq: !!objectEquality
                  };

      初始化了一个watcher对象,用来保存一些监听相关的信息,简单的说明一下

      • fn, 代表监听函数,当监控表达式新旧不相等时会执行此函数
      • last, 保存最后一次发生变化的监控表达式的值
      • get, 保存一个监控表达式对应的函数,目的是用来获取表达式的值然后用来进行新旧对比的
      • exp, 保存一个原始的监控表达式
      • eq, 保存$watch函数的第三个参数,表示是否进行深度比较

        然后会检查传递进来的监听参数是否为函数,如果是一个有效的字符串,则通过parse来解析生成一个函数,否则赋值为一个noop占位函数,最后生成一个包装函数,函数体的内容就是执行刚才生成的监听函数,默认传递当前作用域.

        接着会检查监控表达式是否为字符串并且执行表达式的constant为true,代表这个字符串是一个常量,那么,系统在处理这种监听的时候,执行完一次监听函数之后就会删除这个$watch.最后往当前作用域里的$$watchers数组头中添加$watch信息,注意这里的返回值,利用JS的闭包保留了当前的watcher,然后返回一个函数,这个就是用来删除监听用的.

        $Parse

        $watch在底层很大程度上依赖于$parse,同时也是Angular $ompile的核心依赖方法之一,parse.js里就是$parse的全部代码它的核心是解析字符串,而且默认支持四则运算,运算符号的优先级处理,只是额外的增加了对变量的支持以及过滤器的支持.

        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,生成执行表达式,其实就是返回一个执行函数

          看这里

          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里去

          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方法

          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方法

          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,它保存着所有运算符所对应的动作,比如这里的+,对应的动作是

          '+':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;}
          

          大家注意了,这里有4个参数,可以先透露一下,第一个是传的是当前上下文对象,比喻当前scope实例,这个是为了获取字符串中的变量值,第二个参数是本地变量,是传递给函数当入参用的,基本用不到,最后两个参是关键,+是二元运算符,所以a代表左侧运算值,b代表右侧运算值.

          最后解析完+之后,index停在了2的位置,跟1一样,也是返回一个token,fn属性也是一个返回当前数字的函数.

          当解析完整个1+2字符串后,lex返回的是token数组,这个即可传递给parse来处理,来看看

          var value = json ? this.primary() : this.statements();
          

          默认json是false,所以会跳到this.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(self, locals) {
                          var value;
                          for (var i = 0; i < statements.length; i++) {
                            var statement = statements[i];
                            if (statement) {
                              value = statement(self, locals);
                            }
                          }
                          return value;
                        };
                }
              }
            }
          

          代码以一个无限loop的while开始,语句分析的时候是有运算符优先级的,默认的顺序是,这里以函数名为排序

          filterChain<expression<assignment<ternary<logicalOR<logicalAND<equality<relational<additive<multiplicative<unary<primary

          中文翻译下就是这样的

          过滤函数<一般表达式<赋值语句<三元运算<逻辑or<逻辑and<比较运算<关系运算<加减法运算<乘法运算<一元运算,最后则默认取第一个token的fn属性

          这里以1+2的token为例,这里会用到parse的expect方法,expect会用到peek方法

          peek: function(e1, e2, e3, e4) {
              if (this.tokens.length > 0) {
                var token = this.tokens[0];
                var t = token.text;
                if (t === e1 || t === e2 || t === e3 || t === e4 ||
                    (!e1 && !e2 && !e3 && !e4)) {
                  return token;
                }
              }
              return false;
            },
          
            expect: function(e1, e2, e3, e4){
              var token = this.peek(e1, e2, e3, e4);
              if (token) {
                if (this.json && !token.json) {
                  this.throwError('is not valid json', token);
                }
                this.tokens.shift();
                return token;
              }
              return false;
            }
          

          expect方法传空就是默认从token数组中弹出第一个token,数组数量减1

          1+2的执行语句最后会定位到加法运算那里additive

           additive: function() {
              var left = this.multiplicative();
              var token;
              while ((token = this.expect('+','-'))) {
                left = this.binaryFn(left, token.fn, this.multiplicative());
              }
              return left;
            }
          

          最后返回一个二元操作的函数binaryFn

          binaryFn: function(left, fn, right) {
              return extend(function(self, locals) {
                return fn(self, locals, left, right);
              }, {
                constant:left.constant && right.constant
              });
            }
          

          这个函数参数里的left,right对应的'1','2'两个token的fn属性,即是

          function(){ return number;}
          

          fn函数对应additive方法中+号对应token的fn

          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;}
          

          最后生成执行表达式函数,也就是filterChain返回的left值,被push到statements方法中的statements数组中,仔细看statements方法的返回值,假如表达式数组长度为1,则返回第一个执行表达式,否则返回一个包装的函数,里面是一个loop,不断的执行表达式,只返回最后一个表达式的值

          return (statements.length === 1)
                      ? statements[0]
                      : function(self, locals) {
                          var value;
                          for (var i = 0; i < statements.length; i++) {
                            var statement = statements[i];
                            if (statement) {
                              value = statement(self, locals);
                            }
                          }
                          return value;
                        }
          

          好了,说完了生成执行表达式,其实parse的任务已经完成了,现在只需要把这个作为parseprovider的返回值了.

          $eval

          这个$eval也是挺方便的函数,假如你想直接在程序里执行一个字符串的话,那么可以这么用

          $scope.name = '2';
          $scope.$eval('1+name'); // ==> 会输出12
          

          大家来看看它的函数体

          return $parse(expr)(this, locals);
          

          其实就是通过parse来解析成一个执行表达式函数,然后传递当前作用域以及额外的参数,返回这个执行表达式函数的值

          $evalAsync

          evalAsync函数的作用就是延迟执行表达式,并且执行完不管是否异常,触发dirty check.

           if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
                    $browser.defer(function() {
                      if ($rootScope.$$asyncQueue.length) {
                        $rootScope.$digest();
                      }
                    });
                  }
          
          this.$$asyncQueue.push({scope: this, expression: expr});
          

          可以看到当前作用域内部有一个$$asyncQueue异步队列,保存着所有需要延迟执行的表达式,此处的表达式可以是字符串或者函数,因为这个表达式最终会调用$eval方法,注意这里调用了$browser服务的defer方法,从ng->browser.js源码里可以看到,其实这里就是调用setTimeout来实现的.

          self.defer = function(fn, delay) {
              var timeoutId;
              outstandingRequestCount++;
              timeoutId = setTimeout(function() {
                delete pendingDeferIds[timeoutId];
                completeOutstandingRequest(fn);
              }, delay || 0);
              pendingDeferIds[timeoutId] = true;
              return timeoutId;
            };
          

          上面的代码主要是延迟执行函数,另外pendingDeferIds对象保存所有setTimeout返回的id,这个会在self.defer.cancel这里可以取消执行延迟执行.

          $postDigest

          这个方法跟evalAsync不同的时,它不会主动触发digest方法,只是往postDigestQueue队列中增加执行表达式,它会在digest体内最后执行,相当于在触发dirty check之后,可以执行别的一些逻辑.

          this.$$postDigestQueue.push(fn);

          $digest

          digest方法是dirty check的核心,主要思路是先执行$$asyncQueue队列中的表达式,然后开启一个loop来的执行所有的watch里的监听函数,前提是前后两次的值是否不相等,假如ttl超过系统默认值(在1.5.x版本中 ttl = 10),则dirth check结束,最后执行$$postDigestQueue队列里的表达式.

          $digest: function() {
                  var watch, value, last,
                      watchers,
                      asyncQueue = this.$$asyncQueue,
                      postDigestQueue = this.$$postDigestQueue,
                      length,
                      dirty, ttl = TTL,
                      next, current, target = this,
                      watchLog = [],
                      logIdx, logMsg, asyncTask;
          
                  beginPhase('$digest');
          
                  lastDirtyWatch = null;
          
                  do { // "while dirty" loop
                    dirty = false;
                    current = target;
          
                    while(asyncQueue.length) {
                      try {
                        asyncTask = asyncQueue.shift();
                        asyncTask.scope.$eval(asyncTask.expression);
                      } catch (e) {
                        clearPhase();
                        $exceptionHandler(e);
                      }
                      lastDirtyWatch = null;
                    }
          
                    traverseScopesLoop:
                    do { // "traverse the scopes" loop
                      if ((watchers = current.$$watchers)) {
                        // process our watches
                        length = watchers.length;
                        while (length--) {
                          try {
                            watch = watchers[length];
                            // Most common watches are on primitives, in which case we can short
                            // circuit it with === operator, only when === fails do we use .equals
                            if (watch) {
                              if ((value = watch.get(current)) !== (last = watch.last) &&
                                  !(watch.eq
                                      ? equals(value, last)
                                      : (typeof value == 'number' && typeof last == 'number'
                                         && isNaN(value) && isNaN(last)))) {
                                dirty = true;
                                lastDirtyWatch = watch;
                                watch.last = watch.eq ? copy(value) : value;
                                watch.fn(value, ((last === initWatchVal) ? value : last), current);
                                if (ttl < 5) {
                                  logIdx = 4 - ttl;
                                  if (!watchLog[logIdx]) watchLog[logIdx] = [];
                                  logMsg = (isFunction(watch.exp))
                                      ? 'fn: ' + (watch.exp.name || watch.exp.toString())
                                      : watch.exp;
                                  logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
                                  watchLog[logIdx].push(logMsg);
                                }
                              } else if (watch === lastDirtyWatch) {
                                // If the most recently dirty watcher is now clean, short circuit since the remaining watchers
                                // have already been tested.
                                dirty = false;
                                break traverseScopesLoop;
                              }
                            }
                          } catch (e) {
                            clearPhase();
                            $exceptionHandler(e);
                          }
                        }
                      }
          
                      // Insanity Warning: scope depth-first traversal
                      // yes, this code is a bit crazy, but it works and we have tests to prove it!
                      // this piece should be kept in sync with the traversal in $broadcast
                      if (!(next = (current.$$childHead ||
                          (current !== target && current.$$nextSibling)))) {
                        while(current !== target && !(next = current.$$nextSibling)) {
                          current = current.$parent;
                        }
                      }
                    } while ((current = next));
          
                    // `break traverseScopesLoop;` takes us to here
          
                    if((dirty || asyncQueue.length) && !(ttl--)) {
                      clearPhase();
                      throw $rootScopeMinErr('infdig',
                          '{0} $digest() iterations reached. Aborting!\n' +
                          'Watchers fired in the last 5 iterations: {1}',
                          TTL, toJson(watchLog));
                    }
          
                  } while (dirty || asyncQueue.length);
          
                  clearPhase();
          
                  while(postDigestQueue.length) {
                    try {
                      postDigestQueue.shift()();
                    } catch (e) {
                      $exceptionHandler(e);
                    }
                  }
                }
          

          通过上面的代码,可以看出,核心就是两个loop,外loop保证所有的model都能检测到,内loop则是真实的检测每个watch,watch.get就是计算监控表达式的值,这个用来跟旧值进行对比,假如不相等,则执行监听函数

          注意这里的watch.eq这是是否深度检查的标识,equals方法是angular.js里的公共方法,用来深度对比两个对象,这里的不相等有一个例外,那就是NaN ===NaN,因为这个永远都是false,所以这里加了检查

          !(watch.eq
              ? equals(value, last)
              : (typeof value == 'number' && typeof last == 'number'
                 && isNaN(value) && isNaN(last)))
          

          比较完之后,把新值传给watch.last,然后执行watch.fn也就是监听函数,传递三个参数,分别是:最新计算的值,上次计算的值(假如是第一次的话,则传递新值),最后一个参数是当前作用域实例,这里有一个设置外loop的条件值,那就是dirty = true,也就是说只要内loop执行了一次watch,则外loop还要接着执行,这是为了保证所有的model都能监测一次,虽然这个有点浪费性能,不过超过ttl设置的值后,dirty check会强制关闭,并抛出异常

          if((dirty || asyncQueue.length) && !(ttl--)) {
              clearPhase();
              throw $rootScopeMinErr('infdig',
                  '{0} $digest() iterations reached. Aborting!\n' +
                  'Watchers fired in the last 5 iterations: {1}',
                  TTL, toJson(watchLog));
          }
          

          这里的watchLog日志对象是在内loop里,当ttl低于5的时候开始记录的

          if (ttl < 5) {
              logIdx = 4 - ttl;
              if (!watchLog[logIdx]) watchLog[logIdx] = [];
              logMsg = (isFunction(watch.exp))
                  ? 'fn: ' + (watch.exp.name || watch.exp.toString())
                  : watch.exp;
              logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
              watchLog[logIdx].push(logMsg);
          }
          

          当检查完一个作用域内的所有watch之后,则开始深度遍历当前作用域的子级或者父级

          // Insanity Warning: scope depth-first traversal
          // yes, this code is a bit crazy, but it works and we have tests to prove it!
          // this piece should be kept in sync with the traversal in $broadcast
          if (!(next = (current.$$childHead ||
                (current !== target && current.$$nextSibling)))) {
              while(current !== target && !(next = current.$$nextSibling)) {
                current = current.$parent;
              }
          }
          

          上面的代码其实就是不断的查找当前作用域的子级,没有子级,则开始查找兄弟节点,最后查找它的父级节点,是一个深度遍历查找.只要next有值,则内loop则一直执行

          while ((current = next))
          

          不过内loop也有跳出的情况,那就是当前watch跟最后一次检查的watch相等时就退出内loop.

          else if (watch === lastDirtyWatch) {
              // If the most recently dirty watcher is now clean, short circuit since the remaining watchers
              // have already been tested.
              dirty = false;
              break traverseScopesLoop;
          }
          

          注意这个内loop同时也是一个label(标签)语句,这个可以在loop中执行跳出操作就像上面的break

          正常执行完两个loop之后,清除当前的阶段标识clearPhase();,然后开始执行postDigestQueue队列里的表达式.

          while(postDigestQueue.length) {
              try {
                postDigestQueue.shift()();
              } catch (e) {
                $exceptionHandler(e);
              }
          }
          

          接下来说说,用的也比较多的$apply方法

          $apply

          这个方法一般用在,不在ng的上下文中执行js代码的情况,比如原生的DOM事件中执行想改变ng中某些model的值,这个时候就要使用$apply方法了

          $apply: function(expr) {
              try {
                beginPhase('$apply');
                return this.$eval(expr);
              } catch (e) {
                $exceptionHandler(e);
              } finally {
                clearPhase();
                try {
                  $rootScope.$digest();
                } catch (e) {
                  $exceptionHandler(e);
                  throw e;
                }
              }
          }
          

          代码中,首先让当前阶段标识为$apply,这个可以防止使用$apply方法时检查是否已经在这个阶段了,然后就是执行$eval方法, 这个方法上面有讲到,最后执行$digest方法,来使ng中的M或者VM改变.

          接下来说说scope中event模块,它的api跟一般的event事件模块比较像,提供有$on,$emit,$broadcast,这三个很实用的方法

          $on

          这个方法是用来定义事件的,这里用到了两个实例变量$$listeners,$$listenerCount,分别用来保存事件,以及事件数量计数

          $on: function(name, listener) {
                  var namedListeners = this.$$listeners[name];
                  if (!namedListeners) {
                    this.$$listeners[name] = namedListeners = [];
                  }
                  namedListeners.push(listener);
          
                  var current = this;
                  do {
                    if (!current.$$listenerCount[name]) {
                      current.$$listenerCount[name] = 0;
                    }
                    current.$$listenerCount[name]++;
                  } while ((current = current.$parent));
          
                  var self = this;
                  return function() {
                    namedListeners[indexOf(namedListeners, listener)] = null;
                    decrementListenerCount(self, 1, name);
                  };
                }
          

          分析上面的代码,可以看出每当定义一个事件的时候,都会向$$listeners对象中添加以name为key的属性,值就是事件执行函数,注意这里有个事件计数,只要有父级,则也给父级的$$listenerCount添加以name为key的属性,并且值+1,这个$$listenerCount会在广播事件的时候用到,最后这个方法返回一个取消事件的函数,先设置$$listeners中以name为key的值为null,然后调用decrementListenerCount来使该事件计数-1.

          $emit

          这个方法是用来触发$on定义的事件,原理就是loop$$listeners属性,检查是否有值,有的话,则执行,然后依次往上检查父级,这个方法有点类似冒泡执行事件.

          $emit: function(name, args) {
                  var empty = [],
                      namedListeners,
                      scope = this,
                      stopPropagation = false,
                      event = {
                        name: name,
                        targetScope: scope,
                        stopPropagation: function() {stopPropagation = true;},
                        preventDefault: function() {
                          event.defaultPrevented = true;
                        },
                        defaultPrevented: false
                      },
                      listenerArgs = concat([event], arguments, 1),
                      i, length;
          
                  do {
                    namedListeners = scope.$$listeners[name] || empty;
                    event.currentScope = scope;
                    for (i=0, length=namedListeners.length; i<length; i++) {
          
                      // if listeners were deregistered, defragment the array
                      if (!namedListeners[i]) {
                        namedListeners.splice(i, 1);
                        i--;
                        length--;
                        continue;
                      }
                      try {
                        //allow all listeners attached to the current scope to run
                        namedListeners[i].apply(null, listenerArgs);
                      } catch (e) {
                        $exceptionHandler(e);
                      }
                    }
                    //if any listener on the current scope stops propagation, prevent bubbling
                    if (stopPropagation) return event;
                    //traverse upwards
                    scope = scope.$parent;
                  } while (scope);
          
                  return event;
                }
          

          上面的代码比较简单,首先定义一个事件参数,然后开启一个loop,只要scope有值,则一直执行,这个方法的事件链是一直向上传递的,不过当在事件函数执行stopPropagation方法,就会停止向上传递事件.

          $broadcast

          这个是$emit的升级版,广播事件,即能向上传递,也能向下传递,还能平级传递,核心原理就是利用深度遍历当前作用域

          $broadcast: function(name, args) {
              var target = this,
                  current = target,
                  next = target,
                  event = {
                    name: name,
                    targetScope: target,
                    preventDefault: function() {
                      event.defaultPrevented = true;
                    },
                    defaultPrevented: false
                  },
                  listenerArgs = concat([event], arguments, 1),
                  listeners, i, length;
          
              //down while you can, then up and next sibling or up and next sibling until back at root
              while ((current = next)) {
                event.currentScope = current;
                listeners = current.$$listeners[name] || [];
                for (i=0, length = listeners.length; i<length; i++) {
                  // if listeners were deregistered, defragment the array
                  if (!listeners[i]) {
                    listeners.splice(i, 1);
                    i--;
                    length--;
                    continue;
                  }
          
                  try {
                    listeners[i].apply(null, listenerArgs);
                  } catch(e) {
                    $exceptionHandler(e);
                  }
                }
          
                // Insanity Warning: scope depth-first traversal
                // yes, this code is a bit crazy, but it works and we have tests to prove it!
                // this piece should be kept in sync with the traversal in $digest
                // (though it differs due to having the extra check for $$listenerCount)
                if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
                    (current !== target && current.$$nextSibling)))) {
                  while(current !== target && !(next = current.$$nextSibling)) {
                    current = current.$parent;
                  }
                }
              }
          
              return event;
          }
          

          代码跟$emit差不多,只是跟它不同的时,这个是不断的取next值,而next的值则是通过深度遍历它的子级节点,兄弟节点,父级节点,依次查找可用的以name为key的事件.注意这里的注释,跟$digest里的差不多,都是通过深度遍历查找,所以$broadcast方法也不能常用,性能不是很理想

          $destroy

          这个方法是用来销毁当前作用域,代码主要是清空当前作用域内的一些实例属性,以免执行digest,$emit,$broadcast时会关联到

          $destroy: function() {
              // we can't destroy the root scope or a scope that has been already destroyed
              if (this.$$destroyed) return;
              var parent = this.$parent;
          
              this.$broadcast('$destroy');
              this.$$destroyed = true;
              if (this === $rootScope) return;
          
              forEach(this.$$listenerCount, bind(null, decrementListenerCount, this));
          
              // sever all the references to parent scopes (after this cleanup, the current scope should
              // not be retained by any of our references and should be eligible for garbage collection)
              if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
              if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
              if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
              if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
          
          
              // All of the code below is bogus code that works around V8's memory leak via optimized code
              // and inline caches.
              //
              // see:
              // - https://code.google.com/p/v8/issues/detail?id=2073#c26
              // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909
              // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451
          
              this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead =
                  this.$$childTail = this.$root = null;
          
              // don't reset these to null in case some async task tries to register a listener/watch/task
              this.$$listeners = {};
              this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = [];
          
              // prevent NPEs since these methods have references to properties we nulled out
              this.$destroy = this.$digest = this.$apply = noop;
              this.$on = this.$watch = function() { return noop; };
          }
          

          代码比较简单,先是通过foreach来清空$$listenerCount实例属性,然后再设置$parent,$$nextSibling,$$prevSibling,$$childHead,$$childTail,$root为null,清空$$listeners,$$watchers,$$asyncQueue,$$postDigestQueue,最后就是重罢方法为noop占位函数


Tags:AN NG GU UL 
作者:网络 来源:不断成长的我