Angularjs 脏值检测

文章转自:http://www.ituring.com.cn/article/39865html

 

构建本身的AngularJS,第一部分:Scope和Digest

原文连接:http://teropa.info/blog/2013/11/03/make-your-own-angular-part-1-scopes-and-digest.htmlgit

Angular是一个成熟和强大的JavaScript框架。它也是一个比较庞大的框架,在熟练掌握以前,须要领会它提出的不少新概念。不少Web开发人员涌向Angular,有很多人面临一样的障碍。Digest究竟是怎么作的?定义一个指令(directive)有哪些不一样的方法?Service和provider有什么区别?github

Angular的文档挺不错的,第三方的资源也愈来愈丰富,想要学习一门新的技术,没什么方法比把它拆开研究其运做机制更好。express

在这个系列的文章中,我将从无到有构建AngularJS的一个实现。随着逐步深刻的讲解,读者将能对Angular的运做机制有一个深刻的认识。数组

在第一部分中,读者将看到Angular的做用域是如何运做的,还有好比$eval, $digest, $apply这些东西怎么实现。Angular的脏检查逻辑看上去有些难以想象,但你将看到实际并不是如此。数据结构

基础知识

在Github上,能够看到这个项目的所有源码。相比只复制一份下来,我更建议读者从无到有构建本身的实现,从不一样角度探索代码的每一个步骤。在本文中,我嵌入了JSBin的一些代码,能够直接在文章中进行一些互动。(译者注:由于我在github上翻译,无法集成JSBin了,只能给连接……)app

咱们将使用Lo-Dash库来处理一些在数组和对象上的底层操做。Angular自身并未使用Lo-Dash,可是从咱们的目的看,要尽可能无视这些不太相关的比较底层的事情。当读者在代码中看到下划线(_)的时候,那就是在调用Lo-Dash的功能。框架

咱们还将使用console.assert函数作一些特别的测试。这个函数应该适用于全部现代JavaScript环境。异步

下面是使用Lo-Dash和assert函数的示例:async

http://jsbin.com/UGOVUk/4/embed?js,console

Scope对象

Angular的Scope对象是POJO(简单的JavaScript对象),在它们上面,能够像对其余对象同样添加属性。Scope对象是用构造函数建立的,咱们来写个最简单的版本:

function Scope() { }

如今咱们就可使用new操做符来建立一个Scope对象了。咱们也能够在它上面附加一些属性:

var aScope = new Scope(); aScope.firstName = 'Jane'; aScope.lastName = 'Smith';

这些属性没什么特别的。不须要调用特别的设置器(setter),赋值的时候也没什么限制。相反,在两个特别的函数:$watch和$digest之中发生了一些奇妙的事情。

监控对象属性:$watch和$digest

$watch和$digest是相辅相成的。二者一块儿,构成了Angular做用域的核心:数据变化的响应。

使用$watch,能够在Scope上添加一个监听器。当Scope上发生变动时,监听器会收到提示。给$watch指定以下两个函数,就能够建立一个监听器:

  • 一个监控函数,用于指定所关注的那部分数据。
  • 一个监听函数,用于在数据变动的时候接受提示。

做为一名Angular用户,通常来讲,是监控一个表达式,而不是使用监控函数。监控表达式是一个字符串,好比说“user.firstName”,一般在数据绑定,指令的属性,或者JavaScript代码中指定,它被Angular解析和编译成一个监控函数。在这篇文章的后面部分咱们会探讨这是如何作的。在这篇文章中,咱们将使用稍微低级的方法直接提供监控功能。

为了实现$watch,咱们须要存储注册过的全部监听器。咱们在Scope构造函数上添加一个数组:

function Scope() { this.$$watchers = []; }

在Angular框架中,双美圆符前缀$$表示这个变量被看成私有的来考虑,不该当在外部代码中调用。

如今咱们能够定义$watch方法了。它接受两个函数做参数,把它们存储在$$watchers数组中。咱们须要在每一个Scope实例上存储这些函数,因此要把它放在Scope的原型上:

Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn }; this.$$watchers.push(watcher); };

另一面就是$digest函数。它执行了全部在做用域上注册过的监听器。咱们来实现一个它的简化版,遍历全部监听器,调用它们的监听函数:

Scope.prototype.$digest = function() { _.forEach(this.$$watchers, function(watch) { watch.listenerFn(); }); };

如今咱们能够添加监听器,而后运行$digest了,这将会调用监听函数:

http://jsbin.com/oMaQoxa/2/embed?js,console

这些自己没什么大用,咱们要的是能检测由监控函数指定的值是否确实变动了,而后调用监听函数。

脏值检测

如同上文所述,监听器的监听函数应当返回咱们所关注的那部分数据的变化,一般,这部分数据就存在于做用域中。为了使得访问做用域更便利,在调用监控函数的时候,使用当前做用域做为实参。一个关注做用域上fiestName属性的监听器像这个样子:

function(scope) { return scope.firstName; }

这是监控函数的通常形式:从做用域获取一些值,而后返回。

$digest函数的做用是调用这个监控函数,而且比较它返回的值和上一次返回值的差别。若是不相同,监听器就是脏的,它的监听函数就应当被调用。

想要这么作,$digest须要记住每一个监控函数上次返回的值。既然咱们如今已经为每一个监听器建立过一个对象,只要把上一次的值存在这上面就好了。下面是检测每一个监控函数值变动的$digest新实现:

Scope.prototype.$digest = function() { var self = this; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (newValue !== oldValue) { watch.listenerFn(newValue, oldValue, self); } watch.last = newValue; }); };

对每一个监听器,咱们调用监控函数,把做用域自身看成实参传递进去,而后比较这个返回值和上次返回值,若是不一样,就调用监听函数。方便起见,咱们把新旧值和做用域都看成参数传递给监听函数。最终,咱们把监听器的last属性设置成新返回的值,下一次能够用它来做比较。

有了这个实现以后,咱们就能够看到在$digest调用的时候,监听函数是怎么执行的:

http://jsbin.com/OsITIZu/3/embed?js,console

咱们已经实现了Angular做用域的本质:添加监听器,在digest里运行它们。

也已经能够看到几个关于Angular做用域的重要性能特性:

  • 在做用域上添加数据自己并不会有性能折扣。若是没有监听器在监控某个属性,它在不在做用域上都无所谓。Angular并不会遍历做用域的属性,它遍历的是监听器。

  • $digest里会调用每一个监控函数,所以,最好关注监听器的数量,还有每一个独立的监控函数或者表达式的性能。

在Digest的时候得到提示

若是你想在每次Angular的做用域被digest的时候获得通知,能够利用每次digest的时候挨个执行监听器这个事情,只要注册一个没有监听函数的监听器就能够了。

想要支持这个用例,咱们须要在$watch里面检测是否监控函数被省略了,若是是这样,用个空函数来代替它:

Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() { } }; this.$$watchers.push(watcher); };

若是用了这个模式,须要记住,即便没有listenerFn,Angular也会寻找watchFn的返回值。若是返回了一个值,这个值会提交给脏检查。想要采用这个用法又想避免多余的事情,只要监控函数不返回任何值就好了。在这个例子里,监听器的值始终会是未定义的。

http://jsbin.com/OsITIZu/4/embed?js,console

这个实现的核心就这样,可是离最终的仍是差太远了。好比说有个很典型的场景咱们不能支持:监听函数自身也修改做用域上的属性。若是这个发生了,另外有个监听器在监控被修改的属性,有可能在同一个digest里面检测不到这个变更:

http://jsbin.com/eTIpUyE/2/embed?js,console

咱们来修复这个问题。

当数据脏的时候持续Digest

咱们须要改变一下digest,让它持续遍历全部监听器,直到监控的值中止变动。

首先,咱们把如今的$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个以上链状的监听器。

事实上,Angular里面的TTL是能够调整的。咱们将在后续文章讨论provider和依赖注入的时候再回顾这个话题。

咱们继续,给外层digest循环添加一个循环计数器。若是达到了TTL,就抛出异常:

Scope.prototype.$digest = function() { var ttl = 10; var dirty; do { dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); };

下面是更新过的版本,可让咱们循环引用的监控例子抛出异常:

http://jsbin.com/uNapUWe/2/embed?js,console

这些应当已经把digest的事情说清楚了。

如今,咱们把注意力转到如何检测变动上吧。

基于值的脏检查

咱们曾经使用严格等于操做符(===)来比较新旧值,在绝大多数状况下,它是不错的,好比全部的基本类型(数字,字符串等等),也能够检测一个对象或者数组是否变成新的了,但Angular还有一种办法来检测变动,用于检测当对象或者数组内部产生变动的时候。那就是:能够监控值的变动,而不是引用。

这类脏检查须要给$watch函数传入第三个布尔类型的可选参数当标志来开启。当这个标志为真的时候,基于值的检查开启。咱们来从新定义$watch,接受这个参数,而且把它存在监听器里:

Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var watcher = { watchFn: watchFn, listenerFn: listenerFn, valueEq: !!valueEq }; this.$$watchers.push(watcher); };

咱们所作的一切是把这个标志加在监听器上,经过两次取反,强制转换为布尔类型。当用户调用$watch,没传入第三个参数的时候,valueEq会是未定义的,在监听器对象里就变成了false。

基于值的脏检查意味着若是新旧值是对象或者数组,咱们必须遍历其中包含的全部内容。若是它们之间有任何差别,监听器就脏了。若是该值包含嵌套的对象或者数组,它也会递归地按值比较。

Angular内置了本身的相等检测函数,可是咱们会用Lo-Dash提供的那个。让咱们定义一个新函数,取两个值和一个布尔标志,并比较相应的值:

Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue; } };

为了提示值的变化,咱们也须要改变以前在每一个监听器上存储旧值的方式。只存储当前值的引用是不够的,由于在这个值内部发生的变动也会生效到它的引用上,$$areEqual方法比较同一个值的两个引用始终为真,监控不到变化,所以,咱们须要创建当前值的深拷贝,而且把它们储存起来。

就像相等检测同样,Angular也内置了本身的深拷贝函数,但咱们仍是用Lo-Dash提供的。咱们修改一下$digestOnce,在内部使用新的$$areEqual函数,若是须要的话,也复制最后一次的引用:

Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); }); return dirty; };

如今咱们能够看到两种脏检测方式的差别:

http://jsbin.com/ARiWENO/3/embed?js,console

相比检查引用,检查值的方式显然是一个更为复杂的操做。遍历嵌套的数据结构很花时间,保持深拷贝的数据也占用很多内存。这就是Angular默认不使用基于值的脏检测的缘由,用户须要显式设置这个标记去打开它。

Angular也提供了第三种脏检测的方法:集合监控。就像基于值的检测,也能提示对象和数组中的变动。但不一样于基于值的检测方式,它作的是一个比较浅的检测,并不递归进入到深层去,因此它比基于值的检测效率更高。集合检测是经过“$watchCollection”函数来使用的,在这个系列的后续部分,咱们会来看看它是如何实现的。

在咱们完成值的比对以前,还有些JavaScript怪事要处理一下。

非数字(NaN)

在JavaScript里,NaN(Not-a-Number)并不等于自身,这个听起来有点怪,但确实就这样。若是咱们在脏检测函数里不显式处理NaN,一个值为NaN的监听器会一直是脏的。

对于基于值的脏检测来讲,这个事情已经被Lo-Dash的isEqual函数处理掉了。对于基于引用的脏检测来讲,咱们须要本身处理。来修改一下$$areEqual函数的代码:

Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { if (valueEq) { return _.isEqual(newValue, oldValue); } else { return newValue === oldValue || (typeof newValue === 'number' && typeof oldValue === 'number' && isNaN(newValue) && isNaN(oldValue)); } };

如今有NaN的监听器也正常了:

http://jsbin.com/ijINaRA/2/embed?js,console

基于值的检测实现好了,如今咱们该把注意力集中到应用程序代码如何跟做用域打交道上了。

$eval - 在做用域的上下文上执行代码

在Angular中,有几种方式能够在做用域的上下文上执行代码,最简单的一种就是$eval。它使用一个函数做参数,所作的事情是当即执行这个传入的函数,而且把做用域自身看成参数传递给它,返回的是这个函数的返回值。$eval也能够有第二个参数,它所作的仅仅是把这个参数传递给这个函数。

$eval的实现很简单:

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

$eval的使用同样很简单:

http://jsbin.com/UzaWUC/1/embed?js,console

那么,为何要用这么一种明显不少余的方式去执行一个函数呢?有人以为,有些代码是专门与做用域的内容打交道的,$eval让这一切更加明显。$scope也是构建$apply的一个部分,后面咱们就来说它。

而后,可能$eval最有意思的用法是当咱们不传入函数,而是表达式。就像$watch同样,能够给$eval一个字符串表达式,它会把这个表达式编译,而后在做用域的上下文中执行。咱们将在这个系列的后面部分实现这些。

$apply - 集成外部代码与digest循环

可能Scope上全部函数里最有名的就是$apply了。它被誉为将外部库集成到Angular的最标准的方式,这话有个不错的理由。

$apply使用函数做参数,它用$eval执行这个函数,而后经过$digest触发digest循环。下面是一个简单的实现:

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

$digest的调用放置于finally块中,以确保即便函数抛出异常,也会执行digest。

关于$apply,大的想法是,咱们能够执行一些与Angular无关的代码,这些代码也仍是能够改变做用域上的东西,$apply能够保证做用域上的监听器能够检测这些变动。当人们谈论使用$apply集成代码到“Angular生命周期”的时候,他们指的就是这个事情,也没什么比这更重要的了。

这里是$apply的实践:

http://jsbin.com/UzaWUC/2/embed?js,console

延迟执行 - $evalAsync

在JavaScript中,常常会有把一段代码“延迟”执行的状况 - 把它的执行延迟到当前的执行上下文结束以后的将来某个时间点。最多见的方式就是调用setTimeout()函数,传递一个0(或者很是小)做为延迟参数。

这种模式也适用于Angular程序,但更推荐的方式是使用$timeout服务,而且使用$apply把要延迟执行的函数集成到digest生命周期。

但在Angular中还有一种延迟代码的方式,那就是Scope上的$evalAsync函数。$evalAsync接受一个函数,把它列入计划,在当前正持续的digest中或者下一次digest以前执行。举例来讲,你能够在一个监听器的监听函数中延迟执行一些代码,即便它已经被延迟了,仍然会在现有的digest遍历中被执行。

咱们首先须要的是存储$evalAsync列入计划的任务,能够在Scope构造函数中初始化一个数组来作这事:

function Scope() { this.$$watchers = []; this.$$asyncQueue = []; }

咱们再来定义$evalAsync,它添加将在这个队列上执行的函数:

Scope.prototype.$evalAsync = function(expr) { this.$$asyncQueue.push({scope: this, expression: expr}); };

咱们显式在放入队列的对象上设置当前做用域,是为了使用做用域的继承,在这个系列的下一篇文章中,咱们会讨论这个。

而后,咱们在$digest中要作的第一件事就是从队列中取出每一个东西,而后使用$eval来触发全部被延迟执行的函数:

Scope.prototype.$digest = function() { var ttl = 10; var dirty; do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { throw "10 digest iterations reached"; } } while (dirty); };

这个实现保证了:若是看成用域仍是脏的,就想把一个函数延迟执行,那这个函数会在稍后执行,但还处于同一个digest中。

下面是关于如何使用$evalAsync的一个示例:

http://jsbin.com/ilepOwI/1/embed?js,console

做用域阶段

$evalAsync作的另一件事情是:若是如今没有其余的$digest在运行的话,把给定的$digest延迟执行。这意味着,不管何时调用$evalAsync,能够肯定要延迟执行的这个函数会“很快”被执行,而不是等到其余什么东西来触发一次digest。

须要有一种机制让$evalAsync来检测某个$digest是否已经在运行了,由于它不想影响到被列入计划将要执行的那个。为此,Angular的做用域实现了一种叫作阶段(phase)的东西,它就是做用域上一个简单的字符串属性,存储了如今正在作的信息。

在Scope的构造函数里,咱们引入一个叫$$phase的字段,初始化为null:

function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$phase = null; }

而后,咱们定义一些方法用于控制这个阶段变量:一个用于设置,一个用于清除,也加个额外的检测,以确保不会把已经激活状态的阶段再设置一次:

Scope.prototype.$beginPhase = function(phase) { if (this.$$phase) { throw this.$$phase + ' already in progress.'; } this.$$phase = phase; }; Scope.prototype.$clearPhase = function() { this.$$phase = null; };

在$digest方法里,咱们来从外层循环设置阶段属性为“$digest”:

Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); };

咱们把$apply也修改一下,在它里面也设置个跟本身同样的阶段。在调试的时候,这个会有些用:

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

最终,把对$digest的调度放进$evalAsync。它会检测做用域上现有的阶段变量,若是没有(也没有已列入计划的异步任务),就把这个digest列入计划。

Scope.prototype.$evalAsync = function(expr) { var self = this; if (!self.$$phase && !self.$$asyncQueue.length) { setTimeout(function() { if (self.$$asyncQueue.length) { self.$digest(); } }, 0); } self.$$asyncQueue.push({scope: self, expression: expr}); };

有了这个实现以后,无论什么时候、何地,调用$evalAsync,均可以肯定有一个digest会在不远的未来发生。

http://jsbin.com/iKeSaGi/1/embed?js,console

在digest以后执行代码 - $$postDigest

还有一种方式能够把代码附加到digest循环中,那就是把一个$$postDigest函数列入计划。

在Angular中,函数名字前面有双美圆符号表示它是一个内部的东西,不是应用开发人员应该用的。但它确实存在,因此咱们也要把它实现出来。

就像$evalAsync同样,$$postDigest也能把一个函数列入计划,让它“之后”运行。具体来讲,这个函数将在下一次digest完成以后运行。将一个$$postDigest函数列入计划不会致使一个digest也被延后,因此这个函数的执行会被推迟到直到某些其余缘由引发一次digest。顾名思义,$$postDigest函数是在digest以后运行的,若是你在$$digest里面修改了做用域,须要手动调用$digest或者$apply,以确保这些变动生效。

首先,咱们给Scope的构造函数加队列,这个队列给$$postDigest函数用:

function Scope() { this.$$watchers = []; this.$$asyncQueue = []; this.$$postDigestQueue = []; this.$$phase = null; }

而后,咱们把$$postDigest也加上去,它所作的就是把给定的函数加到队列里:

Scope.prototype.$$postDigest = function(fn) { this.$$postDigestQueue.push(fn); };

最终,在$digest里,当digest完成以后,就把队列里面的函数都执行掉。

Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); while (this.$$postDigestQueue.length) { this.$$postDigestQueue.shift()(); } };

下面是关于如何使用$$postDigest函数的:

http://jsbin.com/IMEhowO/1/embed?js,console

异常处理

现有对Scope的实现已经逐渐接近在Angular中实际的样子了,但还有些脆弱,由于咱们迄今为止没有花精力在异常处理上。

Angular的做用域在遇到错误的时候是很是健壮的:当产生异常的时候,无论在监控函数中,在$evalAsync函数中,仍是在$$postDigest函数中,都不会把digest终止掉。咱们如今的实现里,在以上任何地方产生异常都会把整个$digest弄挂。

咱们能够很容易修复它,把上面三个调用包在try...catch中就行了。

Angular其实是把这些异常抛给了它的$exceptionHandler服务。既然咱们如今尚未这东西,先扔到控制台上吧。

$evalAsync和$$postDigest的异常处理是在$digest函数里,在这些场景里,从已列入计划的程序中抛出的异常将被记录成日志,它后面的仍是正常运行:

Scope.prototype.$digest = function() { var ttl = 10; var dirty; this.$beginPhase("$digest"); do { while (this.$$asyncQueue.length) { try { var asyncTask = this.$$asyncQueue.shift(); this.$eval(asyncTask.expression); } catch (e) { (console.error || console.log)(e); } } dirty = this.$$digestOnce(); if (dirty && !(ttl--)) { this.$clearPhase(); throw "10 digest iterations reached"; } } while (dirty); this.$clearPhase(); while (this.$$postDigestQueue.length) { try { this.$$postDigestQueue.shift()(); } catch (e) { (console.error || console.log)(e); } } };

监听器的异常处理放在$$digestOnce里。

Scope.prototype.$$digestOnce = function() { var self = this; var dirty; _.forEach(this.$$watchers, function(watch) { try { var newValue = watch.watchFn(self); var oldValue = watch.last; if (!self.$$areEqual(newValue, oldValue, watch.valueEq)) { watch.listenerFn(newValue, oldValue, self); dirty = true; } watch.last = (watch.valueEq ? _.cloneDeep(newValue) : newValue); } catch (e) { (console.error || console.log)(e); } }); return dirty; };

如今咱们的digest循环碰到异常的时候健壮多了。

http://jsbin.com/IMEhowO/2/embed?js,console

销毁一个监听器

当注册一个监听器的时候,通常都须要让它一直存在于整个做用域的生命周期,因此不多会要显式把它移除。也有些场景下,须要保持做用域的存在,但要把某个监听器去掉。

Angular中的$watch函数是有返回值的,它是个函数,若是执行,就把刚注册的这个监听器销毁。想在咱们这个版本里实现这功能,只要返回一个函数在里面把这个监控器从$$watchers数组去除就能够了:

Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var self = this; var watcher = { watchFn: watchFn, listenerFn: listenerFn, valueEq: !!valueEq }; self.$$watchers.push(watcher); return function() { var index = self.$$watchers.indexOf(watcher); if (index >= 0) { self.$$watchers.splice(index, 1); } }; };

如今咱们就能够把$watch的这个返回值存起来,之后调用它来移除这个监听器:

http://jsbin.com/IMEhowO/4/embed?js,console

展望将来

咱们已经走了很长一段路了,已经有了一个完美能够运行的相似Angular这样的脏检测做用域系统的实现了,可是Angular的做用域上面还作了更多东西。

或许最重要的是,在Angular里,做用域并非孤立的对象,做用域能够继承于其余做用域,监听器也不只仅是监听本做用域上的东西,还能够监听这个做用域的父级做用域。这种方法,概念上很简单,可是对于初学者常常容易形成混淆。因此,本系列的下一篇文章主题就是做用域的继承。

后面咱们会讨论Angular的事件系统,也是实如今Scope上的。

相关文章
相关标签/搜索