在上一節項目初始化中,咱們最終獲得了一個能夠運行的基礎代碼庫,它的基本結構以下:html
其中node_modules文件夾存放項目中的第三方依賴模塊,src存放咱們的項目代碼源文件,test存放測試用例文件,.jshintrc是jshint插件的配置文件,karma.conf.js是karma的配置文件,package.json是npm的配置文件,結構其實很簡單。從本節開始,會在這個代碼庫的基礎上進行咱們本身Angular的實現。node
首先,在寫代碼以前,在命令行中輸入npm test命令,讓咱們的測試用例代碼實時在後臺進行最新代碼的測試,以便咱們隨時知道咱們的代碼是否符合規範,這一行爲做爲一個後臺任務貫穿於咱們框架實現整個過程,對於測試結果再也不一一列舉,若是出現錯誤須要自行修改代碼讓其符合測試用例的預期。git
scope在Angular中實際上就是一個普通的對象,在該對象中存在各類屬性和方法,同時咱們也能夠本身在該對象上設置屬性。scope的做用主要有如下幾種:github
1)在controllers和views之間共享數據;npm
2) 在應用的各個不一樣部分之間共享數據;json
3)廣播和監聽事件;數組
4)監聽數據的變化;框架
在本文中,咱們首先來從頭實現一個scope及它的digest循環和髒檢查機制,主要經過$watch和$digest兩個方法來實現.函數
首先,在src目錄下建立一個scope.js,用來存放scope實現的相關代碼,同時在test目錄下建立一個scope_spec.js,用來存放與scope相關的測試用例。性能
咱們第一步須要實現的是經過構造函數new出來一個scope實例,在該實例下咱們可以設置相關屬性,本着TDD(測試驅動開發)的思想,咱們首先編寫相關測試用例,而後再進行實現,在test/scope_spec.js中編寫如下代碼:
1 'use strict'; 2 var Scope = require('../src/scope'); 3 describe("Scope", function() { 4 it("can be constructed and used as an object", function() { 5 var scope = new Scope(); 6 scope.aProperty = 1; 7 expect(scope.aProperty).toBe(1); 8 }); 9 });
在該測試用例中咱們引入對於scope的實現,採用new運算符獲得一個scope實例,在該實例上可以添加任何屬性,並在設置屬性以後測試被設置的值是否正確。
在src/scope.js中的實現以下:
1 'use strict'; 2 function Scope() { 3 } 4 module.exports = Scope;
目前的實現很簡單,僅僅是一個構造函數,不須要解釋。
接着,咱們須要在每一個scope實例中實現一個$watch方法,它的做用是監測某個值,當其發生變化的時候調用某個函數進行某項操做,該方法須要兩個參數,第一個參數是一個function,用來返回須要被監測的值(Angular自己的實現中,第一個參數不必定爲function,可爲任意值,此處爲了簡化,暫且讓第一個參數爲function,其餘類型參數的監測,後續會給出實現)。第二個參數爲另外一個function,當被監測的值發生變化的時候,須要調用該函數。在scope中,咱們使用$watch函數設置對於某些值得監測,稱之爲一個watcher,一個scope實例中存在若干watcher,digest循環的做用就是啓動一輪循環,檢查該scope下面的全部watcher,若是發生變化,調用該watcher的函數(即第二個參數)。對於digest,咱們使用scope下面的$digest方法來實現。
按照上述思想,咱們修改test/scope_spec.js文件的內容以下:
1 describe("Scope", function() { 2 it("can be constructed and used as an object", function() { 3 var scope = new Scope(); 4 scope.aProperty = 1; 5 expect(scope.aProperty).toBe(1); 6 }); 7 describe("digest", function() { 8 var scope; 9 beforeEach(function() { 10 scope = new Scope(); 11 }); 12 it("calls the listener function of a watch on first $digest", function() { 13 var watchFn = function() { return 'wat'; }; 14 var listenerFn = jasmine.createSpy(); 15 scope.$watch(watchFn, listenerFn); 16 scope.$digest(); 17 expect(listenerFn).toHaveBeenCalled(); 18 }); 19 }); 20 });
黃色背景部分是發生變化的部分,它定義了一個關於digest的測試用例,在該用例中,每一個測試用來開始的時候,首先new一個scope實例,接着調用該scope下面的$watch方法在其下面設置一個watcher(此處被檢測的值返回的是一個字符串,只是爲了佔位,並不表明被監測的真實值),而後調用$digest方法,調用完畢後,須要肯定該watcher的第二個函數參數是否被調用過,若是被調用過就符合咱們的預期。
這個時候能夠查看後臺的karma報告的錯誤信息,該測試用劉確定是沒法經過的,由於咱們尚未在scope.js中實現這兩個方法。接着在src/scope.js中實現這兩個方法,代碼以下:
1 'use strict'; 2 var _ = require('lodash'); 3 function Scope() { 4 this.$$watchers = []; 5 } 6 Scope.prototype.$watch = function(watchFn, listenerFn) { 7 var watcher = { 8 watchFn: watchFn, 9 listenerFn: listenerFn 10 }; 11 this.$$watchers.push(watcher); 12 }; 13 Scope.prototype.$digest = function() { 14 _.forEach(this.$$watchers, function(watcher) { 15 watcher.listenerFn(); 16 }); 17 };
在上面代碼的第四行,在構造函數中添加了一個$$watchers屬性,用來存放該scope下面的全部watcher,因爲它是一個私有屬性,這裏使用$$前綴來表示,只可以在內部實現代碼中調用。6-12行是$watch方法的實現,它的做用是在該scope下面建立一個watcher,因爲它是個實例方法,因此咱們定義在prototype上。它擁有兩個參數,第一個參數函數返回被監測的值,第二個參數當被檢測的值發生變化後被調用。建立watcher的是指就是將這個watcher對象加入到$$watchers數組中去。13-16行是$digest方法的實現,它的做用是當調用該方法的時候,遍歷該scope下面的全部watcher,並執行其監測函數。
這個時候能夠保存後查看karma報告的測試信息,顯示諸如如下信息:
表示咱們以前的測試用例經過,從此全部的功能開發都基於這種先寫測試用例,後寫實現,而後查看測試結果的模式,此後其餘的測試結果再也不給出。
通常狀況下,咱們須要監測的變化的值都是該scope下面的某個屬性值,這就須要咱們的$watch函數的第一個參數返回值可以獲取到scope實例。基於此,咱們將scope實例做爲參數傳入$watch的第一個參數函數中,編寫測試用例以下test/scope_spec.js:
1 it("calls the watch function with the scope as the argument", function() { 2 var watchFn = jasmine.createSpy(); 3 var listenerFn = function() { }; 4 scope.$watch(watchFn, listenerFn); 5 scope.$digest(); 6 expect(watchFn).toHaveBeenCalledWith(scope); 7 });
在該用例中,咱們但願調用$watch以後,確保它擁有scope做爲其參數,src/scope.js實現以下:
1 Scope.prototype.$digest = function() { 2 var self = this; 3 _.forEach(this.$$watchers, function(watcher) { 4 watcher.watchFn(self); 5 watcher.listenerFn(); 6 }); 7 };
首先第2行存儲this對象,即scope實例對象,而後第4行將其做爲參數傳遞給watchFn並執行。
$digest的方法須要實現的是循環scope下全部的watcher,在某個watcher下面,首先經過watchFn函數獲得被監測的值,將其與上次存儲的值進行比較,若是發生變化,則執行listenerFn。測試用例test/scope_sepc.js以下:
1 it("calls the listener function when the watched value changes", function() { 2 scope.someValue = 'a'; 3 scope.counter = 0; 4 scope.$watch( 5 function(scope) { return scope.someValue; }, 6 function(newValue, oldValue, scope) { scope.counter++; } 7 ); 8 expect(scope.counter).toBe(0); 9 scope.$digest(); 10 expect(scope.counter).toBe(1); 11 scope.$digest(); 12 expect(scope.counter).toBe(1); 13 scope.someValue = 'b'; 14 expect(scope.counter).toBe(1); 15 scope.$digest(); 16 expect(scope.counter).toBe(2); 17 });
在scope下面設置一個someValue對象,並使用$watch方法監測該對象,若是發生變化即newValue不等於oldValue,則執行counter++;只有每次someValue的值發生了變化以後,counter的值纔可以增長。
src/scope.js實現以下:
1 Scope.prototype.$digest = function() { 2 var self = this; 3 var newValue, oldValue; 4 _.forEach(this.$$watchers, function(watcher) { 5 newValue = watcher.watchFn(self); 6 oldValue = watcher.last; 7 if (newValue !== oldValue) { 8 watcher.last = newValue; 9 watcher.listenerFn(newValue, oldValue, self); 10 } 11 }); 12 };
從新修改$digest方法,經過watchFn來獲得newValue,經過存儲在watcher自己的屬性last來記錄上次的值,經過===來比較,若是不相等,則將watcher.last賦值爲newValue,而後再執行listenerFn函數,這個函數的參數newValue表示被檢測的值得最新值,oldValue表示上次的值,self表明scope自己。
接着,咱們知道當第一次初始化一個watcher的時候,它沒有last屬性,只有通過一次比較$digest調用以後,last的值纔不爲空,因此須要初始化watcher的last屬性。
src/scope.js以下:
1 function initWatchVal() { } 2 Scope.prototype.$watch = function(watchFn, listenerFn) { 3 var watcher = { 4 watchFn: watchFn, 5 listenerFn: listenerFn, 6 last: initWatchVal 7 }; 8 this.$$watchers.push(watcher); 9 };
咱們從新定義了$watch方法,爲每一個watcher初始化了一個last值,爲了保證它是一個惟一的值,除了與它自身相等,與其餘任何值都不能相等,咱們採用一個function來初始化它。
在咱們第一次調用$digest方法進行比較newValue和oldValue的時候,這個時候oldValue是initWatchVal即初始值,因此須要額外判斷,若是是初始值,則在listenerFn中將其初始化爲newValue,實現以下src/scope.js:
1 Scope.prototype.$digest = function() { 2 var self = this; 3 var newValue, oldValue; 4 _.forEach(this.$$watchers, function(watcher) { 5 newValue = watcher.watchFn(self); 6 oldValue = watcher.last; 7 if (newValue !== oldValue) { 8 watcher.last = newValue; 9 watcher.listenerFn(newValue, 10 (oldValue === initWatchVal ? newValue : oldValue), 11 self); 12 } 13 }); 14 };
第9-11行實現了對於oldValue參數的初始化,讓它等於oldValue(不是第一次比較),或者等於newValue(第一次比較)。
在某些狀況下,調用$watch函數的時候有可能只傳遞了第一個參數,並無listnerFn,考慮到這種現象,修改scope.js以下:
1 Scope.prototype.$watch = function(watchFn, listenerFn) { 2 var watcher = { 3 watchFn: watchFn, 4 listenerFn: listenerFn || function() { }, 5 last: initWatchVal 6 }; 7 this.$$watchers.push(watcher); 8 };
咱們給listenerFn一個默認的值—空的function,當調用者省略第二個參數也可以正常運行。
考慮到一種極端的狀況是,當咱們在$digest函數中執行某個listenerFn的時候,有可能這個listenerFn自己會修改scope下面的某個屬性值,而這個屬性值又被某個watcher所監測,這樣會致使對於這個watcher的監測不會獲得通知,也不會觸發其listenerFn。因此咱們須要定義$digest的行爲是讓其一直遍歷全部的watcher,直到被監聽的全部watcher的值都中止變化爲止。這個時候咱們須要定義一個$digestOnce函數,它只遍歷一次該scope下的全部watcher,並最終返回一個值表示是否還存在還在發生變化的watcher的值。src/scope.js實現以下:
1 Scope.prototype.$$digestOnce = function() { 2 var self = this; 3 var newValue, oldValue, dirty; 4 _.forEach(this.$$watchers, function(watcher) { 5 newValue = watcher.watchFn(self); 6 oldValue = watcher.last; 7 if (newValue !== oldValue) { 8 watcher.last = newValue; 9 watcher.listenerFn(newValue, 10 (oldValue === initWatchVal ? newValue : oldValue), 11 self); 12 dirty = true; 13 } 14 }); 15 return dirty; 16 };
上述代碼經過返回的dirty值來肯定是否還存在變化。接着咱們修改$digest方法來調用該函數以下:scope.js
1 Scope.prototype.$digest = function() { 2 var dirty; 3 do { 4 dirty = this.$$digestOnce(); 5 } while (dirty); 6 };
一直調用$digestOnce函數,直到返回的dirty值爲false。在這種狀況下,每次$digest只要有一個watcher的值發生變化,則該次遍歷就被標記爲dirty,就要進行新一輪的循環,直到該輪循環中全部watcher的值都沒有發生變化,這個時候才被認爲是穩定了。
在某些極端狀況下,例如兩個watcher互相監測對方的值,這會致使二者返回值都不穩定,這種循環依賴的狀況會致使整個$digest過程沒法中止下來,而一直遍歷全部watcher,這種狀況須要避免。當前的作法是定義一個變量記錄循環的次數,若是超過這個次數,則throw一個error,告訴調用者$digest次數達到上限了,實現以下src/scope.js
1 Scope.prototype.$digest = function() { 2 var ttl = 10; 3 var dirty; 4 do { 5 dirty = this.$$digestOnce(); 6 if (dirty && !(ttl--)) { 7 throw "10 digest iterations reached"; 8 } 9 } while (dirty); 10 };
咱們採起10次爲上限,當次數超過十次的時候,直接拋出錯誤。
考慮一種狀況,當一個scope下面擁有100個watcher的時候,當遍歷全部的watcher的時候,剛好只有第一個是dirty的,其餘都是clean的。可是就是這一個watcher會致使咱們整個一次$digest循環成爲dirty,從而進入到下次循環。在下次循環過程當中,全部watcher都沒有發生變化即爲clean,可是就是這樣一個小小的watcher,會致使咱們須要遍歷200次不一樣的watcher!針對這種狀況,咱們能夠在一次遍歷中標記最後一個爲dirty的watcher,當下次循環遇到的watcher剛好是上次標記的watcher並變成clean的時候,咱們就能夠中止遍歷,而不是繼續進行該次遍歷直到最後。按照這種思想實現以下:scope.js
1 'use strict'; 2 var _ = require('lodash'); 3 var Scope = require('../src/scope'); 4 function Scope() { 5 this.$$watchers = []; 6 this.$$lastDirtyWatch = null; 7 } 8 Scope.prototype.$digest = function() { 9 var ttl = 10; 10 var dirty; 11 this.$$lastDirtyWatch = null; 12 do { 13 dirty = this.$$digestOnce(); 14 if (dirty && !(ttl--)) { 15 throw "10 digest iterations reached"; 16 } 17 } while (dirty); 18 }; 19 Scope.prototype.$$digestOnce = function() { 20 var self = this; 21 var newValue, oldValue, dirty; 22 _.forEach(this.$$watchers, function(watcher) { 23 newValue = watcher.watchFn(self); 24 oldValue = watcher.last; 25 if (newValue !== oldValue) { 26 self.$$lastDirtyWatch = watcher; 27 watcher.last = newValue; 28 watcher.listenerFn(newValue, 29 (oldValue === initWatchVal ? newValue : oldValue), 30 self); 31 dirty = true; 32 } else if (self.$$lastDirtyWatch === watcher) { 33 return false; 34 } 35 }); 36 return dirty; 37 };
第6行在構造函數中定義了一個$$lastDirtyWatch變量來存儲每一輪循環中最後一個被標記爲dirty的watcher,接着在32-34行當循環到一個watcher爲clean的時候,判斷它時候是咱們標記的上一輪循環中最後一個
dirty的watcher,若是是,就不用再循環了,直接跳出循環(在lodash的forEach方法中返回false直接跳出)。
同時在每次在scope下面新加入一個watcher的時候,須要將該scope的$$lastDirtyWatch屬性重置,不然被新加入的watcher並不會被考慮,實現以下scope.js:
1 Scope.prototype.$watch = function(watchFn, listenerFn) { 2 var watcher = { 3 watchFn: watchFn, 4 listenerFn: listenerFn || function() { }, 5 last: initWatchVal 6 }; 7 this.$$watchers.push(watcher); 8 this.$$lastDirtyWatch = null; 9 };
在每次調用$watch方法的時候都須要重置$$lastDirtyWatch屬性。
在咱們的$digest實現中,比較採用的是===這種方式,在JS中對於原始類型這種方式徹底沒有問題,可是對於像數組對象等引用類型,這種方式就存在問題了。例如一個數組一開始是var arr=[1,2],後來變成了arr=[1,2,3],實際上自己發生了變化,可是使用===運算符比較仍是相等的。這就是說咱們以前的比較是一種基於引用的比較,而對於引用類型元素,須要基於值進行比較。因此咱們須要設置一個屬性,表示對於該watcher的比較是基於引用的仍是基於值的(因爲基於值得比較性能消耗較大,因此默認是基於引用的比較)。實現以下:scope.js
1 Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { 2 var watcher = { 3 watchFn: watchFn, 4 listenerFn: listenerFn || function() { }, 5 valueEq: !!valueEq, 6 last: initWatchVal 7 }; 8 this.$$watchers.push(watcher); 9 this.$$lastDirtyWatch = null; 10 };
上述代碼中,當咱們加入一個watcher的時候,採用valueEq參數指定該watcher是基於引用的仍是基於值的比較,使用!!運算符將其轉換爲一個布爾類型。
接着咱們須要定義一個方法,在引用比較的狀況下進行基於引用的比較,不然基於值得比較,實現以下:
1 Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { 2 if (valueEq) { 3 return _.isEqual(newValue, oldValue); 4 } else { 5 return newValue === oldValue; 6 } 7 };
在第3行咱們利用lodash的isEqual方法來進行基於值的比較。
接着咱們在$digestOnce方法中調用$$areEqual方法,以下:
1 Scope.prototype.$$digestOnce = function() { 2 var self = this; 3 var newValue, oldValue, dirty; 4 _.forEach(this.$$watchers, function(watcher) { 5 newValue = watcher.watchFn(self); 6 oldValue = watcher.last; 7 if (!self.$$areEqual(newValue, oldValue, watcher.valueEq)) { 8 self.$$lastDirtyWatch = watcher; 9 watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue); 10 watcher.listenerFn(newValue, 11 (oldValue === initWatchVal ? newValue : oldValue), 12 self); 13 dirty = true; 14 } else if (self.$$lastDirtyWatch === watcher) { 15 return false; 16 } 17 }); 18 return dirty; 19 };
在第7行,利用$$areEqual方法判斷該watcher是否仍是dirty的,若是是就須要深拷貝該watcher下面的newValue做爲其last屬性。
到目前爲止,咱們已經能夠經過$watch函數監聽scope下面的任意屬性值(不管是原始類型仍是引用類型),並啓動$digest循環進行dirty-checking.最後還有一中極端的狀況,就是當咱們監測是指爲NaN的時候,它自己與本身是不相等的,這會致使其永遠是dirty的,須要考慮到這種極端狀況,實現以下:
1 Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) { 2 if (valueEq) { 3 return _.isEqual(newValue, oldValue); 4 } else { 5 return newValue === oldValue || 6 (typeof newValue === 'number' && typeof oldValue === 'number' && 7 isNaN(newValue) && isNaN(oldValue)); 8 } 9 };
在上述代碼中,若是被檢測的值爲NaN,則進行特殊處理,若是oldValue和newValue都是NaN而且都是number,則認爲二者是相等的。
以上就是咱們本身實現的AngularJS中Scope下面的$watch及$digest髒檢查機制的簡易實現,後續章節依然會在此基礎上進行優化和修改。爲了防止篇幅太長,從此只給出重要的測試用例及測試結果。文章的完整代碼點擊這裏能夠進行查看。