深入理解AngularJs-scope的脏检查(一)(2)

$digestOnce的核心逻辑就在$$everyScope方法的循环体内,即遍历scope.$$watchers, 比对新旧值,根据比对结果确定是否执行listenerFn,并向listenerFn中传入newValue, oldValue, scope供开发者获取。

示例代码第18行,watcher.last的赋值证实了上文提到的$watch的第三个参数valueEq的作用。

示例代码第23行,由于$digest循环会一直运行直到没有dirty watcher时,故单次$digest循环通过缓存最后一个dirty的watcher,在下一次$digest循环时如果遇到$$lastDirtyWatcher就停止当前循环。这样做减少了遍历watcher的数量,优化了性能。

 外层循环

在我们的示例中,外层循环即由 $digest来控制。$digest函数主要由do while循环体内调用$digestOnce进行脏检测 以及 对其他一些异步操作的处理组成。代码如下:

// digest循环的外循环,保持循环直到没有脏值为止 Scope.prototype.$digest = function() { var ttl = TTL; var dirty; this.$root.$$lastDirtyWatch = null; this.$beginPhase('$digest'); if(this.$root.$$applyAsyncId) { clearTimeout(this.$root.$$applyAsyncId); this.$$flushApplyAsync(); } do { while (this.$$asyncQueue.length) { try { var asyncTask = this.$$asyncQueue.shift(); asyncTask.scope.$eval(asyncTask.expression); } catch(e) { console.error(e); } } dirty = this.$$digestOnce(); if((dirty || this.$$asyncQueue.length) && !(ttl--)) { this.$clearPhase(); throw TTL + ' digest iterations reached'; } } while (dirty || this.$$asyncQueue.length); this.$clearPhase(); while(this.$$postDigestQueue.length) { try { this.$$postDigestQueue.shift()(); } catch(e) { console.error(e); } } };

在这一节中我们的主要关注点是脏检测,异步任务相关的$$applyAsync,$$flushApplyAsync,$$asyncQueue,$$postDigestQueue之后再做分析。

示例代码第24行,调用$$digestOnce,并把返回值赋值给dirty。在do while循环中,只要dirty为true,那么循环就会一直执行下去,直到dirty的值为 false。这就是脏检测机制的外层循环的实现,是不是觉得其实很简单呢,嘿嘿。

设想一下,某些值可能会在listenerFn中持续被改变并且,无法稳定下来,那势必会出现死循环。为了解决这个问题,AngularJs使用 TTL(time to live)来对循环次数进行控制,超过最大次数,就会throw错误 并 告诉开发者循环可能永远不会稳定。

现在我们把注意力移到代码第26行的 if 代码块上,不难看出,这里是对最大$digest循环次数进行了限制,每执行一次do while循环的循环体,TTL就会自减1。当TTL值为0,再进行循环就会报错。当然咯,这个TTL的值也是能够进行配置的。

现在,相信小伙伴们对$digest循环已经比较清楚了吧~简单来说,dirty-checking就是依赖缓存在scope上的$$watchers和$digest循环来对值进行监听的。有了$digest,当然还需要有手段去触发它咯。

接下来,我们将进入第二部分:触发$digest循环 和 异步任务处理 

$eval

说到触发$digest循环,大部分同学都会想到$apply。要说$apply就需要先说说$eval。

$eval使我们能够在scope的context中执行一段表达式,并允许传入locals object对当前scope context进行修改。

tip:$parse服务能够接受一个表达式或者函数作为参数,经过处理返回一个函数供开发者调用。这个函数有两个参数context object(通常就是scope),locals object(本地对象,常用来覆盖context中的属性)。

Scope.prototype.$eval = function(expr, locals) { return $parse(expr)(this, locals); };

$apply

$apply 方法接收一个expression或者function作为参数,$apply通过$eval函数执行传入的expression 或 function。最终从$rootScope上触发$digest循环。

$apply 被认为是 使AngularJs与第三方库混合使用最标准的方式。初学者朋友刚开始都会遇到用第三方库修改了scope上的属性或者被watch的属性,但并没有触发$digest循环,导致双向绑定失效的问题。此时,$apply就是解决这种情况的良药!

Scope.prototype.$apply = function(expr) { try { this.$beginPhase('$apply'); return this.$eval(expr); } finally { this.$clearPhase(); this.$root.$digest(); } };

$apply本质上,就是用$eval执行了一段表达式,再调用rootScope的$digest方法。

有时候,当我们能够确定我们不需要从rootScope开始进行$digest循环时,我可以调用scope.digest() 来代替 $apply,这样能够带来性能的提升。

 $evalAsync

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wyxyzs.html