Angular.js源码分析之scope作用域

上一篇在最后提到了依赖项中的$rootScope,那部分代码是这样的:

// 创建注入器
// 隐藏了注入机制
// 详见createInjector函数
var injector = createInjector(modules, config.strictDi);
// 注入这几个服务执行bootstrapApply
injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector',
   function bootstrapApply(scope, element, compile, injector) {
    scope.$apply(function() {
      // 进入ng环境(上下文)执行
      element.data('$injector', injector);
      // compile 核心
      // compile返回一个publicLink函数
      // 然后传入scope直接执行
      // 这个scope也就是rootScope
      compile(element)(scope);
    });
  }]
);

本篇就来分析下这个$rootScope。首先他可以被注入,那他是在什么地方加入到注入器缓存中的呢?细心的可能以及发现,在核心模块ng中,有一段这样的代码:

// 继续一堆的provider
$provide.provider({
  $anchorScroll: $AnchorScrollProvider,
  // 省略
  $controller: $ControllerProvider,
  // 省略
  $rootScope: $RootScopeProvider,
  // 省略
});

这里就看到了原来核心是$RootScopeProvider这个玩意搞得。

先看他的代码:

/**
 * rootScope
 * 所有的scope都是rootScope的孩子
 * 这里构造Scope了 以及形成一个链
 */
function $RootScopeProvider() {
  var TTL = 10;
  var $rootScopeMinErr = minErr('$rootScope');
  var lastDirtyWatch = null;
  var applyAsyncId = null;
  
  // 可以配置最大检查次数
  this.digestTtl = function(value) {
    if (arguments.length) {
      TTL = value;
    }
    return TTL;
  };
  
  // 省略

  this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser',
      function($injector, $exceptionHandler, $parse, $browser) {
    // 省略
    function Scope() {
      // 省略
    }

    // 省略

    var $rootScope = new Scope();

    //The internal queues. Expose them on the $rootScope for debugging/testing purposes.
    // 脏检查队列
    // 异步队列
    var asyncQueue = $rootScope.$$asyncQueue = [];
    // 后脏检查队列
    var postDigestQueue = $rootScope.$$postDigestQueue = [];
    // apply的异步队列们
    var applyAsyncQueue = $rootScope.$$applyAsyncQueue = [];

    return $rootScope;
    
    // 省略
  }];
}

正式因为此才可以注入$rootScope$rootScopeScope的实例,下边就来仔细分析下他(注意上边代码中的几个变量asyncQueue,postDigestQueue,applyAsyncQueue,后边分析要用到的)。

Scope

先看其构造函数部分:

/**
 * Scope构造函数
 * 主要形成一个链(树形)
 * 有父子双重关系$parent $$childHead
 * 还有兄弟间关系 $$nextSibling $$prevSibling
 */
function Scope() {
  this.$id = nextUid();
  this.$$phase = this.$parent = this.$$watchers =
                 this.$$nextSibling = this.$$prevSibling =
                 this.$$childHead = this.$$childTail = null;
  this.$root = this;
  this.$$destroyed = false;
  this.$$listeners = {};
  this.$$listenerCount = {};
  this.$$watchersCount = 0;
  this.$$isolateBindings = null;
}

注意为啥this.$root = this;,这是因为只有rootScope是通过Scope构造函数的实例,其他scope(除了独立Scope,但是他也是会更新他的$root的)全是通过ChildScope构造函数的实例,下边分析$new的时候会分析。

再来看Scope的原型:

Scope.prototype = {
  constructor: Scope,
  /**
   * 创建当前scope的子Scope实例
   */
  $new: function(isolate, parent) {
    // 省略
  },

  $watch: function(watchExp, listener, objectEquality, prettyPrintExpression) {
    // 省略
  },

  $watchGroup: function(watchExpressions, listener) {
    // 省略
  },

  $watchCollection: function(obj, listener) {
    // 省略
  },

  $digest: function() {
    // 省略
  },

  $destroy: function() {
    // 省略
  },

  $eval: function(expr, locals) {
    return $parse(expr)(this, locals);
  },

  $evalAsync: function(expr, locals) {
    // 省略
  },

  $$postDigest: function(fn) {
    postDigestQueue.push(fn);
  },

  $apply: function(expr) {
    // 省略
  },

  $applyAsync: function(expr) {
    // 省略
  },

  $on: function(name, listener) {
    // 省略
  },

  $emit: function(name, args) {
    // 省略
  },

  $broadcast: function(name, args) {
    // 省略
  }
};

可以看出还是有很多方法的,下边就一个个来分析。

$new

具体代码:

/**
 * 创建当前scope的子Scope实例
 */
$new: function(isolate, parent) {
  var child;

  parent = parent || this;

  if (isolate) {
    child = new Scope();
    child.$root = this.$root;
  } else {
    // Only create a child scope class if somebody asks for one,
    // but cache it to allow the VM to optimize lookups.
    if (!this.$$ChildScope) {
      this.$$ChildScope = createChildScopeClass(this);
    }
    child = new this.$$ChildScope();
  }
  child.$parent = parent;
  child.$$prevSibling = parent.$$childTail;
  if (parent.$$childHead) {
    parent.$$childTail.$$nextSibling = child;
    parent.$$childTail = child;
  } else {
    parent.$$childHead = parent.$$childTail = child;
  }

  // When the new scope is not isolated or we inherit from `this`, and
  // the parent scope is destroyed, the property `$$destroyed` is inherited
  // prototypically. In all other cases, this property needs to be set
  // when the parent scope is destroyed.
  // The listener needs to be added after the parent is set
  if (isolate || parent != this) child.$on('$destroy', destroyChildScope);

  return child;
}

可以看出主要就是创建新的Scope的,创建的同时会维护整个的Scope链,如果在Scope构造函数的注释中写的一样,不只有父子关系,还有兄弟关系。

里边还用到了createChildScopeClass这个方法:

function createChildScopeClass(parent) {
  function ChildScope() {
    this.$$watchers = this.$$nextSibling =
        this.$$childHead = this.$$childTail = null;
    this.$$listeners = {};
    this.$$listenerCount = {};
    this.$$watchersCount = 0;
    this.$id = nextUid();
    this.$$ChildScope = null;
  }
  ChildScope.prototype = parent;
  return ChildScope;
}

逻辑很简单就是返回一个ChildScope构造函数,他的原型是传入的parent

$watch

代码:

// 牛逼的watch
// 检测表达式值的变化
$watch: function(watchExp, listener, objectEquality, prettyPrintExpression) {
  // 求取表达式值的函数
  var get = $parse(watchExp);

  if (get.$$watchDelegate) {
    return get.$$watchDelegate(this, listener, objectEquality, get, watchExp);
  }
  var scope = this,
      array = scope.$$watchers,
      watcher = {
        fn: listener,
        last: initWatchVal,
        get: get,
        exp: prettyPrintExpression || watchExp,
        eq: !!objectEquality // 是否深度比较
      };

  lastDirtyWatch = null;

  if (!isFunction(listener)) {
    watcher.fn = noop;
  }

  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);
  incrementWatchersCount(this, 1);

  return function deregisterWatch() {
    if (arrayRemove(array, watcher) >= 0) {
      incrementWatchersCount(scope, -1);
    }
    lastDirtyWatch = null;
  };
}
// 更新count数
// 会同步的更新parent
function incrementWatchersCount(current, count) {
  do {
    current.$$watchersCount += count;
  } while ((current = current.$parent));
}
function decrementListenerCount(current, count, name) {
  do {
    current.$$listenerCount[name] -= count;

    if (current.$$listenerCount[name] === 0) {
      delete current.$$listenerCount[name];
    }
  } while ((current = current.$parent));
}

简单来说其实就是往当前scope的$$watchers中添加一条新的watch信息,最后返回取消watch的一个函数。

$watchGroup

上代码:

$watchGroup: function(watchExpressions, listener) {
  var oldValues = new Array(watchExpressions.length);
  var newValues = new Array(watchExpressions.length);
  var deregisterFns = [];
  var self = this;
  var changeReactionScheduled = false;
  var firstRun = true;

  if (!watchExpressions.length) {
    // 表达式长度为0的话 我们就尽快调用一次listener
    var shouldCall = true;
    self.$evalAsync(function() {
      if (shouldCall) listener(newValues, newValues, self);
    });
    return function deregisterWatchGroup() {
      shouldCall = false;
    };
  }

  if (watchExpressions.length === 1) {
    // Special case size of one
    return this.$watch(watchExpressions[0], function watchGroupAction(value, oldValue, scope) {
      newValues[0] = value;
      oldValues[0] = oldValue;
      listener(newValues, (value === oldValue) ? newValues : oldValues, scope);
    });
  }
  // 循环表达式 单个watch
  forEach(watchExpressions, function(expr, i) {
    var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) {
      newValues[i] = value;
      oldValues[i] = oldValue;
      if (!changeReactionScheduled) {
        // 发生改变就尽快执行watchGroupAction
        // 保证不会重复执行
        changeReactionScheduled = true;
        self.$evalAsync(watchGroupAction);
      }
    });
    deregisterFns.push(unwatchFn);
  });

  function watchGroupAction() {
    changeReactionScheduled = false;

    if (firstRun) {
      firstRun = false;
      listener(newValues, newValues, self);
    } else {
      listener(newValues, oldValues, self);
    }
  }

  return function deregisterWatchGroup() {
    while (deregisterFns.length) {
      deregisterFns.shift()();
    }
  };
}

顾名思义,监控一组表达式,任意一个发生了变化就会调用listener,返回的依旧是取消这组watch的函数。

$watchCollection

代码:

$watchCollection: function(obj, listener) {
  $watchCollectionInterceptor.$stateful = true;

  var self = this;
  // the current value, updated on each dirty-check run
  var newValue;
  // a shallow copy of the newValue from the last dirty-check run,
  // updated to match newValue during dirty-check run
  var oldValue;
  // a shallow copy of the newValue from when the last change happened
  var veryOldValue;
  // only track veryOldValue if the listener is asking for it
  var trackVeryOldValue = (listener.length > 1);
  // 侦查改变次数
  var changeDetected = 0;
  // 改变侦察器
  // 每次watch做检查的时候都会调用changeDetector
  // 而changeDetector其实就会去求得新的值
  // 然后调用$watchCollectionInterceptor用来得到最终的求得
  // 代理出来的值是多少 这里只是返回改变次数
  // 如果次数改变了也就说明集合发生了改变了
  var changeDetector = $parse(obj, $watchCollectionInterceptor);
  var internalArray = [];
  var internalObject = {};
  var initRun = true;
  var oldLength = 0;

  function $watchCollectionInterceptor(_value) {
    newValue = _value;
    var newLength, key, bothNaN, newItem, oldItem;

    // If the new value is undefined, then return undefined as the watch may be a one-time watch
    if (isUndefined(newValue)) return;

    if (!isObject(newValue)) { // if primitive
      // 原始值 很好办 直接判断
      if (oldValue !== newValue) {
        oldValue = newValue;
        changeDetected++;
      }
    } else if (isArrayLike(newValue)) {
      // 类数组
      if (oldValue !== internalArray) {
        // 不是数组到数组的过程
        oldValue = internalArray;
        oldLength = oldValue.length = 0;
        changeDetected++;
      }

      newLength = newValue.length;

      if (oldLength !== newLength) {
        // 长度不相等当然改变了
        changeDetected++;
        oldValue.length = oldLength = newLength;
      }
      
      for (var i = 0; i < newLength; i++) {
        oldItem = oldValue[i];
        newItem = newValue[i];
        // NaN判断
        bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
        if (!bothNaN && (oldItem !== newItem)) {
          如果说某项不相等
          changeDetected++;
          oldValue[i] = newItem;
        }
      }
    } else {
      if (oldValue !== internalObject) {
        // 非对象到对象
        oldValue = internalObject = {};
        oldLength = 0;
        changeDetected++;
      }
      // copy the items to oldValue and look for changes.
      newLength = 0;
      for (key in newValue) {
        if (hasOwnProperty.call(newValue, key)) {
          // 遍历对象的key
          newLength++;
          newItem = newValue[key];
          oldItem = oldValue[key];

          if (key in oldValue) {
            // 如果说这个key在oldValue中是存在的
            // 那么需要比较他们的值
            bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
            if (!bothNaN && (oldItem !== newItem)) {
              changeDetected++;
              oldValue[key] = newItem;
            }
          } else {
            // 否则的话 就是新加的key
            oldLength++;
            oldValue[key] = newItem;
            changeDetected++;
          }
        }
      }
      if (oldLength > newLength) {
        // 删除了一些key的话
        changeDetected++;
        for (key in oldValue) {
          if (!hasOwnProperty.call(newValue, key)) {
            oldLength--;
            delete oldValue[key];
          }
        }
      }
    }
    return changeDetected;
  }
  
  // 改变了的回调
  function $watchCollectionAction() {
    // 回调
    if (initRun) {
      // 是初始化的时候运行的 也就是第一次
      initRun = false;
      listener(newValue, newValue, self);
    } else {
      listener(newValue, veryOldValue, self);
    }

    // 做拷贝,为了下次发生改变 作比较
    if (trackVeryOldValue) {
      if (!isObject(newValue)) {
        //primitive
        veryOldValue = newValue;
      } else if (isArrayLike(newValue)) {
        veryOldValue = new Array(newValue.length);
        for (var i = 0; i < newValue.length; i++) {
          veryOldValue[i] = newValue[i];
        }
      } else { // if object
        veryOldValue = {};
        for (var key in newValue) {
          if (hasOwnProperty.call(newValue, key)) {
            veryOldValue[key] = newValue[key];
          }
        }
      }
    }
  }
  
  // watch
  return this.$watch(changeDetector, $watchCollectionAction);
}

watch一个集合的情况,也就是说要监控的对象obj的任意属性的值发生变化了就会调用listener,老规矩返回取消watch的函数。

$digest

大名鼎鼎的脏检查,代码:

// 脏检查 查找变化核心
// 一次次检查 直至查找不到不同的值了
// 所以可能会陷入无限循环
// 这里ng设置了默认10次的检查
// 如果10次之后还不OK 那么就会报错了
$digest: function() {
  var watch, value, last,
      watchers,
      length,
      dirty, ttl = TTL,
      next, current, target = this,
      watchLog = [],
      logIdx, logMsg, asyncTask;
  // 设置状态 当前正在进行脏检查
  beginPhase('$digest');
  // Check for changes to browser url that happened in sync before the call to $digest
  $browser.$$checkUrlChange();

  if (this === $rootScope && applyAsyncId !== null) {
    // 如果现在有将要执行的异步apply对象的话
    // 此时直接执行 把之前的异步取消掉
    $browser.defer.cancel(applyAsyncId);
    // 直接执行apply异步队列
    // 异步apply中传入的任务(函数)
    flushApplyAsync();
  }

  lastDirtyWatch = null;

  do { // "while dirty" loop
    dirty = false;
    current = target;

    while (asyncQueue.length) {
      // 执行异步队列
      // 异步求值中传入的那些表达式
      try {
        asyncTask = asyncQueue.shift();
        asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
      } catch (e) {
        $exceptionHandler(e);
      }
      lastDirtyWatch = null;
    }

    // 标签语句
    // 用于直接终止循环
    traverseScopesLoop:
    do { // "traverse the scopes" loop
      if ((watchers = current.$$watchers)) {
        // 遍历watchers
        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
            // 直接比较 如果是对象的话 用的是 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;
                // 保存上一个脏的watch
                // 用于下一个条件判断(其实是下一次循环的判断)
                lastDirtyWatch = watch;
                // 上一次值设置为了当前value值
                watch.last = watch.eq ? copy(value, null) : value;
                // 调用watch的监控回调函数listener
                watch.fn(value, ((last === initWatchVal) ? value : last), current);
                if (ttl < 5) {
                  // 记录watch记录
                  // 用于超出TTL次数后输出错误日志
                  logIdx = 4 - ttl;
                  if (!watchLog[logIdx]) watchLog[logIdx] = [];
                  watchLog[logIdx].push({
                    msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp,
                    newVal: value,
                    oldVal: last
                  });
                }
              } else if (watch === lastDirtyWatch) {
                // 上次不相等的值的watch和这次的watch是相等了的
                // 而且已经检查到了上次是脏的watch的地方
                // 连他都不是脏的了 当然可以认为此时 已经是不脏的状态了
                // 也没必要继续循环下去了
                dirty = false;
                break traverseScopesLoop;
              }
            }
          } catch (e) {
            $exceptionHandler(e);
          }
        }
      }

      // 遍历该作用域下面的所有子作用域(深度优先)
      // 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.$$watchersCount && 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--)) {
      // 超出了TTL次数报错
      clearPhase();
      throw $rootScopeMinErr('infdig',
          '{0} $digest() iterations reached. Aborting!\n' +
          'Watchers fired in the last 5 iterations: {1}',
          TTL, watchLog);
    }

  } while (dirty || asyncQueue.length);
  
  // 清除标记
  clearPhase();

  // 现在开始执行 后脏检查队列中的函数 求值
  while (postDigestQueue.length) {
    try {
      postDigestQueue.shift()();
    } catch (e) {
      $exceptionHandler(e);
    }
  }
}

// 执行所有的apply异步队列中函数
function flushApplyAsync() {
  while (applyAsyncQueue.length) {
    try {
      applyAsyncQueue.shift()();
    } catch (e) {
      $exceptionHandler(e);
    }
  }
  applyAsyncId = null;
}

看到脏检查的原理,硬性的循环判断这次的值和上次的值是否一样,不一样就代表脏了,继续check,直至不再发生改变。

$destroy

可以看到Scope会做脏检查之类的,成本还是很高的,所以再不需要的时候就直接销毁掉是很有必要的,不但节省内存还能提高效率,代码:

$destroy: function() {
  // We can't destroy a scope that has been already destroyed.
  if (this.$$destroyed) return;
  var parent = this.$parent;
  
  // 广播下 我要销毁了
  this.$broadcast('$destroy');
  this.$$destroyed = true;

  if (this === $rootScope) {
    // $rootScope都要销毁了 整个应用其实也就毁了
    $browser.$$applicationDestroyed();
  }
  
  incrementWatchersCount(this, -this.$$watchersCount);
  for (var eventName in this.$$listenerCount) {
    decrementListenerCount(this, this.$$listenerCount[eventName], eventName);
  }

  // 更新整个scope链
  if (parent && parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
  if (parent && parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
  if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
  if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;

  // Disable listeners, watchers and apply/digest methods
  this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop;
  this.$on = this.$watch = this.$watchGroup = function() { return noop; };
  this.$$listeners = {};

  // 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 = this.$$watchers = null;
}

$eval

很多场景下可能直接需要求得某个表达式的值 此时直接parse 然后计算得到值就OK,代码:

$eval: function(expr, locals) {
  return $parse(expr)(this, locals);
}

$evalAsync

有时候希望等会再去求得某些表达式的值(其实不一定值求值 有可能是只是执行xx),代码:

// 异步求值
// 往asyncQueue异步队列中加入新的一条
// 如果当前没有正在进行检查 那么就会setTimeout之后做脏检查
$evalAsync: function(expr, locals) {
  // if we are outside of an $digest loop and this is the first time we are scheduling async
  // task also schedule async auto-flush
  if (!$rootScope.$$phase && !asyncQueue.length) {
    $browser.defer(function() {
      if (asyncQueue.length) {
        $rootScope.$digest();
      }
    });
  }

  asyncQueue.push({scope: this, expression: expr, locals: locals});
}

可以看出这个是在下次做脏检查的时候才会去求值的。如果说你想再$apply中再次修改数据的话,这时候你就可以放心大胆的用$evalAsync了,因为不会破坏当前的这次脏检查,避免了因更改数据而使得脏检查进入无限检查的状态。

$apply

广为流传的方法,主要应用在从其他上下文要进入到angular的上下文中的时候。看代码:

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

可以看出调用$apply的话会调用$rootScope的脏检查方法,也就意味着所有的scope的watch都会被检查。

$applyAsync

异步(等会用timeout)来执行表达式。一般用在当有多个表达式需要在一次脏检查中按照顺序进行求值的场景下。

$applyAsync: function(expr) {
  var scope = this;
  expr && applyAsyncQueue.push($applyAsyncExpression);
  scheduleApplyAsync();

  function $applyAsyncExpression() {
    scope.$eval(expr);
  }
}

function flushApplyAsync() {
  while (applyAsyncQueue.length) {
    try {
      applyAsyncQueue.shift()();
    } catch (e) {
      $exceptionHandler(e);
    }
  }
  applyAsyncId = null;
}
function scheduleApplyAsync() {
  if (applyAsyncId === null) {
    applyAsyncId = $browser.defer(function() {
      $rootScope.$apply(flushApplyAsync);
    });
  }
}

到这里你可能已经搞不清楚到底该用$evalAsync还是$applyAsync了,这里有篇文章可以帮你理解:

Scope.$applyAsync() vs. Scope.$evalAsync()

$on, $emit, $broadcast

这三个也就是Scope的监听事件,触发事件(冒泡),以及广播事件(往子Scope上)。具体代码这里就不展示了。

原理的话和普通的时间中心差不多,只是这里会check当前Scope的子Scope们或者父Scope们。

结语

本篇详细分析了下angular中Scope作用域这个东西,主要完成脏检查核心的,同时提供了一些方便好用的方法帮助开发者处理逻辑。值得注意的是上边的代码中多次用了$parse这个东西,具体代码还没有分析,但是我准备放在最后来分析他。

回到本篇最开始位置,看到在invoke的代码其实很简单,调用$rootScope$apply,代码:

scope.$apply(function() {
  // 进入ng环境(上下文)执行
  element.data('$injector', injector);
  // compile 核心
  // compile返回一个publicLink函数
  // 然后传入scope直接执行
  // 这个scope也就是rootScope
  compile(element)(scope);
});

可以看到直接调用了compile,下一篇就来分析分析angular复杂的编译过程。

欢迎吐槽!

发布于: 2015年 10月 22日