简介
在ng的生态中scope处于一个核心的地位,ng对外宣称的双向绑定的底层其实就是scope实现的,本章主要对scope的watch机制、继承性以及事件的实现作下分析。
监听
1. $watch
1.1 使用
// $watch: function(watchExp, listener, objectEquality)
var unwatch = $scope.$watch('aa', function () {}, isEqual);
使用过angular的会经常这上面这样的代码,俗称“手动”添加监听,其他的一些都是通过插值或者directive自动地添加监听,但是原理上都一样。
1.2 源码分析
function(watchExp, listener, objectEquality) {
var scope = this,
// 将可能的字符串编译成fn
get = compileToFn(watchExp, 'watch'),
array = scope.$$watchers,
watcher = {
fn: listener,
last: initWatchVal, // 上次值记录,方便下次比较
get: get,
exp: watchExp,
eq: !!objectEquality // 配置是引用比较还是值比较
};
lastDirtyWatch = null;
if (!isFunction(listener)) {
var listenFn = compileToFn(listener || noop, 'listener');
watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);};
}
if (!array) {
array = scope.$$watchers = [];
}
// 之所以使用unshift不是push是因为在 $digest 中watchers循环是从后开始
// 为了使得新加入的watcher也能在当次循环中执行所以放到队列最前
array.unshift(watcher);
// 返回unwatchFn, 取消监听
return function deregisterWatch() {
arrayRemove(array, watcher);
lastDirtyWatch = null;
};
}
</div>
从代码看 $watch 还是比较简单,主要就是将 watcher 保存到 $$watchers 数组中
2. $digest
当 scope 的值发生改变后,scope是不会自己去执行每个watcher的listenerFn,必须要有个通知,而发送这个通知的就是 $digest
2.1 源码分析
整个 $digest 的源码差不多100行,主体逻辑集中在【脏值检查循环】(dirty check loop) 中, 循环后也有些次要的代码,如 postDigestQueue 的处理等就不作详细分析了。
脏值检查循环,意思就是说只要还有一个 watcher 的值存在更新那么就要运行一轮检查,直到没有值更新为止,当然为了减少不必要的检查作了一些优化。
代码:
// 进入$digest循环打上标记,防止重复进入
beginPhase('$digest');
lastDirtyWatch = null;
// 脏值检查循环开始
do {
dirty = false;
current = target;
// asyncQueue 循环省略
traverseScopesLoop:
do {
if ((watchers = current.$$watchers)) {
length = watchers.length;
while (length--) {
try {
watch = watchers[length];
if (watch) {
// 作更新判断,是否有值更新,分解如下
// value = watch.get(current), last = watch.last
// value !== last 如果成立,则判断是否需要作值判断 watch.eq?equals(value, last)
// 如果不是值相等判断,则判断 NaN的情况,即 NaN !== NaN
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;
// 缓存last值
watch.last = watch.eq ? copy(value, null) : value;
// 执行listenerFn(newValue, lastValue, scope)
// 如果第一次执行,那么 lastValue 也设置为newValue
watch.fn(value, ((last === initWatchVal) ? value : last), current);
// ... watchLog 省略
if (watch.get.$$unwatch) stableWatchesCandidates.push({watch: watch, array: watchers});
}
// 这边就是减少watcher的优化
// 如果上个循环最后一个更新的watch没有改变,即本轮也没有新的有更新的watch
// 那么说明整个watches已经稳定不会有更新,本轮循环就此结束,剩下的watch就不用检查了
else if (watch === lastDirtyWatch) {
dirty = false;
break traverseScopesLoop;
}
}
} catch (e) {
clearPhase();
$exceptionHandler(e);
}
}
}
// 这段有点绕,其实就是实现深度优先遍历
// A->[B->D,C->E]
// 执行顺序 A,B,D,C,E
// 每次优先获取第一个child,如果没有那么获取nextSibling兄弟,如果连兄弟都没了,那么后退到上一层并且判断该层是否有兄弟,没有的话继续上退,直到退到开始的scope,这时next==null,所以会退出scopes的循环
if (!(next = (current.$$childHead ||
(current !== target && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} while ((current = next));
// break traverseScopesLoop 直接到这边
// 判断是不是还处在脏值循环中,并且已经超过最大检查次数 ttl默认10
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); // 循环结束
// 标记退出digest循环
clearPhase();
</div>
上述代码中存在3层循环
第一层判断 dirty,如果有脏值那么继续循环
do {
// ...
} while (dirty)
第二层判断 scope 是否遍历完毕,代码翻译了下,虽然还是绕但是能看懂
do {
// ....
if (current.$$childHead) {
next = current.$$childHead;
} else if (current !== target && current.$$nextSibling) {
next = current.$$nextSibling;
}
while (!next && current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
} while (current = next);
第三层循环scope的 watchers
length = watchers.length;
while (length--) {
try {
watch = watchers[length];
// ... 省略
} catch (e) {
clearPhase();
$exceptionHandler(e);
}
}
3. $evalAsync
3.1 源码分析
$evalAsync用于延迟执行,源码如下:
function(expr) {
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
$browser.defer(function() {
if ($rootScope.$$asyncQueue.length) {
$rootScope.$digest();
}
});
}
this.$$asyncQueue.push({scope: this, expression: expr});
}
</div>
通过判断是否已经有 dirty check 在运行,或者已经有人触发过$evalAsync
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length)
$browser.defer 就是通过调用 setTimeout 来达到改变执行顺序
$browser.defer(function() {
//...
});
</div>
如果不是使用defer,那么
function (exp) {
queue.push({scope: this, expression: exp});
this.$digest();
}
scope.$evalAsync(fn1);
scope.$evalAsync(fn2);
// 这样的结果是
// $digest() > fn1 > $digest() > fn2
// 但是实际需要达到的效果:$digest() > fn1 > fn2
</div>
上节 $digest 中省略了了async 的内容,位于第一层循环中

