$digest脏检查,先看下源码
//位置:rootScope.js
$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');//设置$$phase状态为$digest
// Check for changes to browser url that happened in sync before the call to $digest
$browser.$$checkUrlChange();
lastDirtyWatch = null;
do { // "while dirty" loop
dirty = false;
current = target;
//首先执行由$scope.$evalAsync注册的异步对象
while(asyncQueue.length) {
try {
asyncTask = asyncQueue.shift();
asyncTask.scope.$eval(asyncTask.expression);
} catch (e) {
clearPhase();
$exceptionHandler(e);
}
lastDirtyWatch = null;
}
traverseScopesLoop:
do { // 对当前$scope以及子$scope循环
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, null) : value;
//执行watcher的监听函数
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
//$$childHead 第一个子元素,$$nextSibling 邻近向后的兄弟节点 ,$$childTail最后一个子元素
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--)) {
//如果超过默认10次的$digest的限制,那么终止$digest循环
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) {//执行postDigest序列
try {
postDigestQueue.shift()();
} catch (e) {
$exceptionHandler(e);
}
}
},
分析下主要的对象:
watcher
:是数组,包含多个watch对象
这是watch
对象的值:
//rootScope.js -> $watch
{
fn: listener, //监听函数
last: initWatchVal, // 上次的值
get: get, //获取监听表达式的值
exp: watchExp,//监听表达式
eq: !!objectEquality//是否需要深度比对,$watch函数的第三个参数
};
$watch和$watchCollection的区别
$watch
和$watchCollection
都是监控值的变化。$watch
有三个参数,第三个参数表明是否可以深检查,会检查对象是否完全相等,就是$digest
中的equals
比较。$watchCollection
内部调用的是$watch
,区别就是$watchCollection
是浅监视,只检查对象或数组的第一层,对于更深层的不会再比较。所以当有时候使用$watchCollection
效率会更高于$watch(expr,listener,true)
;
$apply,$eval,$evalAsync
$apply
的作用是手动触发$digest
循环,看下源码:
$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
可以传入参数,返回执行$eval
方法来解析表达式,最后调用$digest
;来看下$eval
方法:
$eval: function(expr, locals) {
return $parse(expr)(this, locals);
},
可以看出$eval
等同于$parse
;
而$evalAsync
有着很大用处,首先看下源码:
$evalAsync: function(expr) {
// 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 && !$rootScope.$$asyncQueue.length) {
$browser.defer(function() {
if ($rootScope.$$asyncQueue.length) {
$rootScope.$digest();
}
});
}
this.$$asyncQueue.push({scope: this, expression: expr});
}
如果当前不处于$digest
或者$apply
的过程中(只有在$apply
和$digest
方法中才会设置$$phase
这个字段),并且asyncQueue
数组中还不存在任务时,就会异步调度一轮digest
循环来确保asyncQueue
数组中的表达式会被执行。如果目前已经处于一轮digest
循环中,那么它能够确保你定义的任务在本轮digest
循环期间一定会被执行!因此,这个过程和浏览器就没有任何关系了,这样能够提高浏览器的渲染效率,因为无效的渲染被屏蔽了。如果当前不在一轮digest循环中,和$timeout就几乎没有区别了。
关于$timeout
和$evalAsync
,在Stackoverlow上有比较好的一个总结,简单的翻译一下:
1. 如果在directive中使用$evalAsync
,那么它的运行时机在Angular对DOM进行操作之后,浏览器渲染之前。
2. 如果在controller中使用$evalAsync
,那么它的运行时机在Angular对DOM进行操作之前,同时也在浏览器渲染之前 - 很少需要这样做。
3. 如果通过$timeout
来异步执行代码,那么它的运行时机在Angular对DOM进行操作之后,也在浏览器渲染完毕之后(这也许会造成页面闪烁)。
因此,我们可以记住一个结论:使用$evalAsync
的最佳场合是在指令的link方法中。这样能够避免浏览器不必要的渲染而造成的页面闪烁。当你在directive中考虑使用$timeout
时,不妨试试$evalAsync
。