[AngularJS面面观] 3. scope中的Dirty Checking(脏数据检查) ---
Digest Cycle中的优化在上一篇文章中,介绍了Digest Cycle的实现方法$digest的大概逻辑。但是离真正的实现还有相当大的差距,具体的实现比较长,而且其中有很多细节在本篇文章还不会介绍,所以就不贴在这里了,有兴趣的可以去看源码。 现在 var ttl = 10;
do {
var dirty = false;
var length = $$watchers.length;
var watcher;
for(var idx = 0; idx < length; idx++) {
watcher = $$watchers[idx];
newVal = watcher.watchFn(scope);
oldVal = watcher.last;
if (!newVal.equals(oldVal)) {
watcher.last = newVal;
watcher.listener(newVal,oldVal,scope);
dirty = true;
}
}
ttl -= 1;
if (dirty && ttl === 0) {
throw '10 $digest() iterations reached. Aborting!';
}
} while (dirty);
1. 减少watchExp执行次数的优化措施从上面的逻辑中可以发现,DC至少会执行一遍。如果有任何listener被执行的话,那么DC至少会执行两遍。因此,当一个scope中绑定的watchers数量为1000时,最终会执行2000次与之关联的watchExp。这可是一笔不小的开销。所以我们需要尽可能地减少调用watchExp的次数。 那么如何减少?在上一篇文章中举了一个例子: 所以我们可以通过记录最后一个把数据弄“脏”的watcher,来达到减少执行次数的目的。当第二次执行这个watcher时,判断它是否是“干净的”,如果它同时也是上一轮循环中把数据弄“脏”的那个watcher。那么可以认为此轮DC到这里就结束了。下面举个例子来说明它的正确性和有效性: 假设我们有10个watcher,记为[w1,w2,……,w9,w10]: 要知道这只是有10个watcher的情况,实际应用中watcher的数量只会远远超过这个值。因此平均而言该优化也能够减少 领会到了该优化的思想,改进代码就不是难事了。伪代码如下所示: var ttl = 10;
var lastDirty;
do {
var dirty = false;
var length = $$watchers.length;
var watcher;
for(var idx = 0; idx < length; idx++) {
watcher = $$watchers[idx];
newVal = watcher.watchFn(scope);
oldVal = watcher.last;
if (!newVal.equals(oldVal)) {
watcher.last = newVal;
watcher.listener(newVal,scope);
dirty = true;
lastDirty = watcher;
} else if(lastDirty === watcher) {
break;
}
}
ttl -= 1;
if (dirty && ttl === 0) {
throw '10 $digest() iterations reached. Aborting!';
}
} while (dirty);
但是,这就完事了吗?就这段逻辑,我们来和angular的实现比较一下: $watch: function(watchExp,listener,objectEquality,prettyPrintExpression) {
......
lastDirtyWatch = null;
......
return function deregisterWatch() {
if (arrayRemove(array,watcher) >= 0) {
incrementWatchersCount(scope,-1);
}
lastDirtyWatch = null;
};
}
以上是一 我们不妨考虑一下当在执行DC的时候,watcher数量发生变化会导致什么结果。比如当我们在某个listener中添加了一个watcher时,这个watcher的加入可能会导致某个watcher不能被执行,原因是这样的: angular实现DC的循环体: do { // "traverse the scopes" loop
if ((watchers = current.$$watchers)) {
// process our watches
length = watchers.length;
while (length--) {
try {
watch = watchers[length];
......
可以发现在执行while循环的时候,循环的次数就已经明确了。即使某个listener中又添加了一个watcher,循环的次数是不会改变的。所以总会有一个watcher在这轮DC中不会执行到。而这个watcher就是发生添加watcher行为的watcher的下一个。 原本的watchers数组是这样的:[w1,w3,w4,w5]。 2. 处理NaN带来的infinite digest loop问题了解JavaScript语法规则的同学们对下面这个判断应该都有印象: NaN === NaN // false
当时看到false这个结果时,差点没吐出一口老血。为什么自己和自己比较还可以不想等。 毕竟,是否执行listener的依据就是比较两个值是否相等。而如果不对NaN做特殊处理那么每次都会调用listener,watcher永远没有稳定的那一天。结果就是不断的抛出 因此,需要对NaN单独做出判断: typeof value === 'number' &&
typeof last === 'number'&&
isNaN(value) && isNaN(last)
当满足上面的判断时,也认为两个值是相等的。这样就不会造成错误地调用listener了。 3. 使用reversed while循环方式进行watchers的遍历循环的方式我们一般认为倒序的会快一点,同时while循环也比for循环要快一点。这里有相关讨论,有兴趣的同学可以看看。 所以在angular的实现中,也采用的是逆序遍历结合while循环的方式。具体到我们的伪代码中: var ttl = 10;
var lastDirty;
do {
var dirty = false;
var length = $$watchers.length;
var watcher;
while(length--) {
watcher = $$watchers[length];
newVal = watcher.watchFn(scope);
oldVal = watcher.last;
if (!newVal.equals(oldVal)) {
watcher.last = newVal;
watcher.listener(newVal,scope);
dirty = true;
lastDirty = watcher;
} else if(lastDirty === watcher) {
break;
}
}
ttl -= 1;
if (dirty && ttl === 0) {
throw '10 $digest() iterations reached. Aborting!';
}
} while (dirty);
当然,除了性能方面的考量外。使用逆序遍历还有一个很重要的考虑:当一个watcher将其自身移除后,不要影响到后续watcher的执行! 举个例子: var deregisterB = scope.$watch(function() {
deregisterB();
});
在watchExp中执行移除操作后,数组就变成了:[A,C]。 然而,当我们采用逆序遍历后,就不存在这个问题了: 4. 不暴露watcher的初值—initWatchVal另外一个细节是,在目前的listener处理逻辑中,watcher的初值是会被暴露出去的。在上一篇文章中,我们讨论了angular为了保证第一次执行watchExp时总会触发listener,会将初值设置为一个function。而显然我们在触发listener时,不需要将该function作为oldValue暴露出去: watcher.listener(newVal,((last === initWatchVal) ? newVal: oldVal),scope); 当前值等于初始值时,直接将当前值作为前值作为参数调用listener。 5. 异常处理目前,DC的核心代码还不够健壮,如果调用watchExp或者listener的过程中出现了异常,那么整个DC就跪了。这显然是不行的,所以需要添加try-catch来保证即使某个watcher出现了问题,也不会影响到其它的watchers。加上了异常处理及以上各种细节后的DC核心代码如下所示: var ttl = 10;
var lastDirty;
do {
var dirty = false;
var length = $$watchers.length;
var watcher;
while(length--) {
try {
watcher = $$watchers[length];
newVal = watcher.watchFn(scope);
oldVal = watcher.last;
if (!newVal.equals(oldVal)) {
watcher.last = newVal;
watcher.listener(newVal,scope);
dirty = true;
lastDirty = watcher;
} else if(lastDirty === watcher) {
break;
}
} catch(e) {
console.error(e);
}
}
ttl -= 1;
if (dirty && ttl === 0) {
throw '10 $digest() iterations reached. Aborting!';
}
} while (dirty);
在下一篇文章中,会介绍和DC相关的其它方法和细节,比如 感谢大家花费宝贵时间阅读我的文章,如果发现文中有不妥之处,请赐教!谢谢大家。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |