首先,我们把现在的$digest函数改名为$$digestOnce,它把所有的监听器运行一次,返回一个布尔值,表示是否还有变更了:
Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = newValue; }); return dirty; };
然后,我们重新定义$digest,它作为一个“外层循环”来运行,当有变更发生的时候,调用$$digestOnce:
Scope.prototype.$digest = function() { var dirty; do { dirty = this.$$digestOnce(); } while (dirty); };
$digest现在至少运行每个监听器一次了。如果第一次运行完,有监控值发生变更了,标记为dirty,所有监听器再运行第二次。这会一直运行,直到所有监控的值都不再变化,整个局面稳定下来了。
Angular作用域里并不是真的有个函数叫做$$digestOnce,相反,digest循环都是包含在$digest里的。我们的目标更多是清晰度而不是性能,所以把内层循环封装成了一个函数。
下面是新的实现:
http://jsbin.com/Imoyosa/3/embed?js,console
我们现在可以对Angular的监听器有另外一个重要认识:它们可能在单次digest里面被执行多次。这也就是为什么人们经常说,监听器应当是幂等的:一个监听器应当没有边界效应,或者边界效应只应当发生有限次。比如说,假设一个监控函数触发了一个Ajax请求,无法确定你的应用程序发了多少个请求。
在我们现在的实现中,有一个明显的遗漏:如果两个监听器互相监控了对方产生的变更,会怎样?也就是说,如果状态始终不会稳定?这种情况展示在下面的代码里。在这个例子里,$digest调用被注释掉了,把注释去掉看看发生什么情况:
http://jsbin.com/eKEvOYa/3/embed?js,console
JSBin执行了一段时间之后就停止了(在我机器上大概跑了100,000次左右)。如果你在别的东西比如Node.js里跑,它会一直运行下去。
放弃不稳定的digest
我们要做的事情是,把digest的运行控制在一个可接受的迭代数量内。如果这么多次之后,作用域还在变更,就勇敢放手,宣布它永远不会稳定。在这个点上,我们会抛出一个异常,因为不管作用域的状态变成怎样,它都不太可能是用户想要的结果。
迭代的最大值称为TTL(short for Time To Live)。这个值默认是10,可能有点小(我们刚运行了这个digest 100,000次!),但是记住这是一个性能敏感的地方,因为digest经常被执行,而且每个digest运行了所有的监听器。用户也不太可能创建10个以上链状的监听器。