咱們將從實現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請求,咱們就不能保證你的應用匯總有多少個請求了。