咱们将从实现AngularJS的一个核心模块 – Scopes – 开始。Scopes被用在许多不一样的方面。 编程
在上面列出的这几项中,最后一项毫无疑问是最有意思的一项。AngularJS scopes实现了一个叫作 dirty-checking的机制,当scope中的一块数据发生改变时,你可以获得通知。你能够照它的样子去实现它,可是它同时也是神秘的 data-binding(数据绑定) 的秘密所在,也是AngularJS的一个主要卖点。 数组
AngularJS的scopes就是通常的JavaScript对象,在它上面你能够绑定你喜欢的属性和其余 对象,然而,它们同时也被添加了一些功能用于观察数据结构上的变化。这些观察的功能都由dirty-checking来实现而且都在一个digest循环中被执行。这就是咱们在本章中须要实现的功能。 数据结构
咱们经过在一个Scope构造函数上面使用new操做符来建立scopes。返回的结果是一个普通的JavaScript对象。咱们如今就对这个基本的行为进行测试。 框架
建立一个test/scope_spec.js文件,并将下面的测试代码添加到其中: 函数
test/scope_spec.js ------- /* jshint globalstrict: true */ /* global Scope: false */ 'use strict'; describe("Scope", function() { it("can be constructed and used as an object", function() { var scope = new Scope(); scope.aProperty = 1; expect(scope.aProperty).toBe(1); }); });
在文件的顶部咱们启用了ES5的严格模式,同时让JSHint知道咱们能够在这个文件中引用一个叫作Scope的全局对象。 grunt
这个测试用来建立一个Scope,并在它上面赋一个任意值,而后检查它是否真正被赋值。 性能
在这里你可能会注意到咱们竟然使用Scope做为一个全局函数。这绝对不是一个好的JavaScript编程方式!在本书的后面,一旦咱们实现了依赖注入,咱们将会改正这个错误。 测试
若是你已经在一个终端中使用了grunt watch,在你添加完这个测试文件以后你会发现它出现了错误,缘由在于咱们如今尚未实现Scope。而这正是咱们想要的,测试驱动开发的第一个重要步骤就是首先要看到错误。
在本书中我都会假设测试套件会自动执行,同时在测试应该执行时我并不会明确的指出。 this
咱们能够轻松的让这个测试经过:建立src/scope.js文件而后在其中添加如下内容: spa
src/scope.js ------ /* jshint globalstrict: true */ 'use strict'; function Scope() { }
在这个测试中,咱们将一个属性(aProperty)赋值给了这个scope。这正是Scope上的属性运行的方式。它们就是正常的JavaScript属性,并无什么特别之处。这里你彻底不须要去调用一个特别的setter,也不须要对你赋值的类型进行什么限制。真正的魔法在于两个特别的函数:$watch和$digest。咱们如今就来看看这两个函数。
$watch和$digest是同一个硬币的两面。它们两者同时造成了$digest循环的核心:对数据的变化作出反应。
你可使用$watch函数为scope添加一个监视器。当这个scope中有变化发生时,监视器便会提醒你。你能够经过给$watch提供两个函数来建立一个监视器:
做为一个AngularJS用户,你实际上常常指明一个监视表达式而不是一个监视函数。一个监视表达式是一个字符串,例如”user.firstName”,就像你在一个数据绑定,一个指令属性,或者在一段JavaScript代码中指明的那样。它会在AngularJS内部被解析而后编译成一个监视函数。咱们将在本书的第二部分中实现这一点。在那以前咱们都将使用底层的方法来直接提供一个监视函数。
硬币的另一面是$digest函数。它迭代了全部绑定到scope中的监视器,而后进行监视并运行相应的监听函数。
为了实现这一块功能,咱们首先来定义一个测试文件并断言你可使用$watch来注册一个监视器,而且当有人调用了$digest的时候监视器的监听函数会被调用。
为了让事情简单一些,咱们将在scope_spec.js文件中添加一个嵌套的describe块。并建立一个beforeEach函数来初始化这个scope,以便咱们能够在进行每一个测试时重复它:
test/scope_spec.js ------ describe("Scope", function() { it("can be constructed and used as an object", function() { var scope = new Scope(); scope.aProperty = 1; expect(scope.aProperty).toBe(1); }); describe("digest", function() { var scope; beforeEach(function() { scope = new Scope(); }); it("calls the listener function of a watch on first $digest", function() { var watchFn = function() { return 'wat'; }; var listenerFn = jasmine.createSpy(); scope.$watch(watchFn, listenerFn); scope.$digest(); expect(listenerFn).toHaveBeenCalled(); }); }); });
在上面的这个测试中咱们调用了$watch来在这个scope上注册一个监视器。咱们如今对于监视函数自己并无什么兴趣,所以咱们随便提供了一个函数来返回一个常数值。做为监听函数,咱们提供了一个Jasmine Spy。接着咱们调用了$digest并检查这个监听器是否真正被调用。
spy是一个Jasmine的术语,它用来模拟一个函数。它让咱们能够方便的回答诸如“这个函数有没有被调用?”以及“这个函数使用了那个参数”这样的问题。
要让这个测试经过的话,咱们还有一些事情须要去作。首先,这个Scope须要有一些地方去存储全部被注册的监视器。咱们如今就在Scope构造函数中添加一个数组存储它们:
src/scope.js ----- function Scope(){ this.$$watchers = []; }
上面代码中的$$前缀在AngularJS框架中被认为是私有变量,它们不该该在应用的外部被调用。
如今咱们能够来定义$watch函数了。它接收两个函数做为参数,而且将它们储存在$$watchers数组中。咱们想要每个Scope对象都拥有这个函数,所以咱们将它添加到Scope的原型中:
src/scope.js ----- Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn }; this.$$watchers.unshift(watcher); };
最后咱们应该有一个$digest函数。如今,咱们来定义一个$digest函数的简化版本,它仅仅只是会迭代全部的注册监视器并调用它们的监听函数:
src/scope.js ----- Scope.prototype.$digest = function() { var length = this.$$watchers.length; var watcher; while (length--) { watcher = this.$$watchers[length]; watcher.listenerFn(); } };
注意到咱们在开始时正向添加监视器数组而后逆序迭代它。这样的作法将会让咱们在实现移除监视器时轻松一点。
此时,测试会经过可是这个版本的$digest并无什么实际做用。咱们真正想要的是检查监视函数指定的值是否发生了变化,发生变化时才会调用监听函数。这叫作dirty-checking。
正如前面所描述的,一个监视器的监视函数应该返回一块咱们感兴趣而且发生变化的数据。一般来讲,这块数据应该是存在于scope中的某个东西。为了让监视函数更方便的访问scope,咱们想要将当前的scope做为监视函数的一个参数来调用它。一个关于firstName属性的监视函数应该以下所示:
---- function(scope){ return scope.firstName; } -----
这就是监视函数一般的样子:从scope中提取一些值而后将它返回。
如今咱们来添加一个测试来检查这个scope确实被提供做为监视函数的一个参数:
test/scope_spec.js ---- it("calls the watch function with the scope as the argument",function(){ var watchFn = jasmine.createSpy(); var listenerEn = function(){}; scope.$watch(watchFn,listenerFn); scope.$digest(); expect(watchFn).toHaveBeenCalledWith(scope); });
这一次咱们为监视函数建立了一个Spy并使用它来检查watch的调用状况。使测试经过的最简单的方法是修改$digest,以下所示:
src/scope.js ---- Scope.prototype.$digest = function(){ var length = this.$$watcher.length; var watcher; while(length--){ watcher = this.$$watchers[length]; watcher.watchFn(this); watcher.listenerFn(); } };
固然,这也不是完整版的$digest函数。$digest函数的职责是调用监视函数并将它的返回值与上一次的返回值进行对比。若是两次的返回值不一样,那么监视器就是dirty的,而且他的监听器函数应该被调用。咱们如今来添加一个测试用例:
test/scope_spec.js --- it("calls the listener function when the watched value changes", function() { scope.someValue = 'a'; scope.counter = 0; scope.$watch( function(scope) { return scope.someValue; }, function(newValue, oldValue, scope) { scope.counter++; } ); expect(scope.counter).toBe(0); scope.$digest(); expect(scope.counter).toBe(1); scope.$digest(); expect(scope.counter).toBe(1); scope.someValue = 'b'; expect(scope.counter).toBe(1); scope.$digest(); expect(scope.counter).toBe(2); });
咱们首先在scope上绑定了两个值:一个字符串和一个数字。咱们接着能够绑定一个监视器来监视这个字符串同时在字符串发生变化时增长这个数字。咱们指望的是在第一次$digest时计数器会增长一次,而后若是值发生变化的话每次后续的$digest都会使计数器增长一次。
注意咱们同时也指定了监听函数:和监视函数同样,它也接受这个scope做为一个参数。它同时也接受这个监视器的新值和旧值。这让应用开发者可以更加轻松的检查究竟发生了什么变化。
为了正常运行,$digest必须记住每一个监视函数的上一个值是什么。既然咱们对每一个监视器已经有了一个对象,咱们能够很方便的在这里储存这个值。下面的代码是一个新版本的$digest的定义,它会检查每一个监视函数的值的变化:
src/scope.js --- Scope.prototype.$digest = function(){ var length = this.$$watcher.length; var watcher, newValue, oldvalue; while(length--){ watcher = this.$$watchers[length]; newValue = watcher.watchFn(this); oldValue = watcher.last; if(newValue !== oldValue){ watch.last = newValue; watch.listenFn(newValue, oldValue, this); } } };
对于每一个监视器,咱们将监视函数的返回值同咱们在last属性中存储的值进行对比。若是两者有区别,咱们就会调用监听器函数,将新值和旧值都传递给它,同时也将scope自己传递给它。最后,咱们将监视器的last属性设置为新的返回值,以便咱们在下一次也能进行比较。
咱们如今已经实现了Angular scopes的核心部分:绑定监视器函数以及将它们在一个digest中运行。
咱们同时也了解到了Angular scopes中几个重要的表现行为:
将一个监视函数的返回值同存储在last中的属性进行比较在大多数状况下是有效的,可是当监视器函数第一次执行时会是什么情形呢?既然此时咱们尚未设置last属性,它的值的undefined。在此时监视器的合法值为undefined也不能使程序正常运行:
test/scope_spec.js --- it("calls listener when watch value is first undefined", function() { scope.counter = 0; scope.$watch( function(scope) { return scope.someValue; }, function(newValue, oldValue, scope) { scope.counter++; } ); scope.$digest(); expect(scope.counter).toBe(1); });
咱们也应该在这里调用监听器函数。咱们须要作的事情是将last属性初始化一个独有的值,这个值要和监视函数可能返回的值都不一样。
有一个函数很适合处理这里的情形,由于JavaScript中的函数是所谓的引用值 - 它们除了本身谁都不相等。咱们在scope.js中引入一个函数:
src/scope.js ---- function initWatchVal(){}
如今咱们将这个函数绑定到新监视器的last属性上:
src/scope.js ---- Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn, last: initWatchVal }; this.$$watchers.unshift(watcher); };
使用这种方法新的监视器将老是能够调用监听器函数,不管监视函数会返回什么。
若是你在一个Angular scope被digest的时候想要得到通知,你能够利用在每次digest时全部的监视函数都要被执行这一点:你只须要注册一个没有监听函数的监视函数便可。咱们将这一点添加到测试中。
tesr/scope_src.js it("may have watchers that omit the listener function", function() { var watchFn = jasmine.createSpy().and.returnValue('something'); scope.$watch(watchFn); scope.$digest(); expect(watchFn).toHaveBeenCalled(); });
这个监听函数并不须要返回任何东西,可是它能够,在这个例子中它也返回了一些东西。当这个scope在digest时,咱们目前实现的代码会抛出一个错误。这是由于咱们试图去掉调用一个并不存在的监听函数。为了给这个情形添加支持,咱们须要在$watch中检查监听器是否被省略,若是是的话,在其中放入一个空函数:
src/scope.js ------ Scope.prototype.$watch = function(watchFn,listenerFn){ var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, last: initWatchVal }; this.$$watchers.unshift(watcher); };
若是你使用了这个模式,必定要记住Angular会查看watchFn的返回值,即便不存在listenerFn。若是你返回一个值,这个值会在dirty-checking的检查范围以内。为了确认你对这种模式的用法不会引发额外的麻烦,不要返回任何东西就能够。在上面的例子中这个监视器将会恒为undefined。
咱们如今已经实现了核心的部分,可是咱们离真正的Angular还远得很。例如,咱们如今的代码还不支持一种典型的场景:监听函数自己会改变scope中的属性。若是这种状况发生了,咱们须要用另外一个监视器来查看属性有没有变化,它在同一个digest循环中可能并不会注意到属性的变化:
tesr/scope_spec.js ---- it("triggers chained watchers in the same digest", function() { scope.name = 'Jane'; scope.$watch( function(scope) { return scope.nameUpper; }, function(newValue, oldValue, scope) { if (newValue) { scope.initial = newValue.substring(0, 1) + '.'; } } ); scope.$watch( function(scope) { return scope.name; }, function(newValue, oldValue, scope) { if (newValue) { scope.nameUpper = newValue.toUpperCase(); } } ); scope.$digest(); expect(scope.initial).toBe('J.'); scope.name = 'Bob'; scope.$digest(); expect(scope.initial).toBe('B.'); });
咱们在这个scope中有两个监视器:一个用来监视nameUpper属性,而且根据它来为initial赋值,另外一个监视name属性并为根据它来为nameUpper赋值。咱们指望的是当scope中的name发生变化时,nameUpper和initial属性都会在digest中相应的发生变化。可是,这种状况目前尚未实现。
咱们很细心的排列监视器以便依赖的监视器能够首先被注册。若是顺序反过来,测试将会立刻经过,由于监视器将会以正确的顺序发生。然而,监视器之间的依赖关系并不依赖于它们的注册顺序。咱们立刻将会看到这一点。
咱们如今要作的事情就是修改digest以便它可以持续的迭代全部监视函数,直到被监视的值中止变化。多作几回digest是咱们可以得到运用于监视器并依赖于其余监视器的变化。
首先,咱们将目前的$digest重命名为$$digestOnce,而且调整它以便它可以在全部监视器上运行一遍,而后返回一个布尔值来讲明有没有任何变化:
src/scope.js ---- Scope.prototype.$$digestOnce = function(){ var length = this.$$watchers.length; var watcher, newValue, oldValue, dirty; while(length--){ watcher = this.$$watchers[length]; newValue = watcher.watchFn(this); oldValue= watcher.last; if(newValue !== oldValue){ watcher.last == newValue; watcher.listenerFn(newValue, oldValue, this); dirty = true; } } return dirty; };
接着,咱们重定义$digest以便它可以运行“外循环”,在变化发生时调用$$digestOnce:
src/scope.js ----- Scope.prototype.$digest = function(){ var dirty; do { dirty = this.$$digestOnce(); } while (dirty); };
$digest如今会在全部的监视器上至少运行一次。若是,在第一次循环后,被监视的值改变了,那么此次循环被标记为dirty,全部的监视器将会运行第二次。循环将会一直进行到没有任何监视值发生变化而且状态稳定为止。
Angular scope中实际上并无一个叫作$$digestOnce的函数。相反,全部的digest循环都嵌套在$digest中。咱们在这里的目的是说明方法,所以咱们故意将内部循环抽取出来成为一个单独的函数。
咱们如今能够来编写另外一个Angular监视函数中重要的观察者了:在每次digest循环中它必需要运行屡次。这就是人们常常说监视器应该知足幂等性的缘由:一个监视函数应该没有任何的反作用,或者在运行任意次数时都会发生反作用。举例来讲,若是一个监视函数触发了一个Ajax请求,咱们就不能保证你的应用汇总有多少个请求了。