深刻理解AngularJs-scope(一)

進入正文前的說明:本文中的示例代碼並不是AngularJs源碼,而是來自書籍<<Build Your Own AngularJs>>, 這本書的做者僅依賴jquery和lodash一步一步構建出AngularJs的各核心模塊,對全面理解AngularJs有很是巨大的幫助。如有正在使用AngulaJs攻城拔寨而且但願徹底掌握手中武器的小夥伴,請前往 https://teropa.info/build-your-own-angular 進行購買閱讀,相信能對你理解AngularJs帶來莫大幫助,感謝做者。javascript

在這篇文章中,但願能讓您理清楚如下幾項與scope相關的功能:java

  1. dirty-checking(髒檢測)核心機制,主要包括:$watch 和 $digest;
  2. 幾種不一樣的觸發$digest循環的方式:$eval, $apply, $evalAsync, $applyAsync;
  3. scope的繼承機制以及isolated scope;
  4. 依賴於scope的事件循環:$on, $broadcast, $emit.

如今開始咱們的第一部分:scope和dirty-checkingjquery

  dirty-checking(髒檢測)原理簡述:scope經過$watch方法向this.$$watchers數組中添加watcher對象(包含watchFn, listenerFn, valueEq, last 四個屬性)。每當$digest循環被觸發時,它會遍歷$$watchers數組,執行watcher中的watchFn,獲取當前scope上某屬性的值(一個watcher對應scope上一個被監聽屬性),而後去同watcher中的last(上一次的值)作比較,若兩值不相等,就執行listenerFn。ajax

 1 function Scope() {
 2     this.$$watchers = [];  // 監聽器數組
 3     this.$$lastDirtyWatch = null; // 每次digest循環的最後一個髒的watcher, 用於優化digest循環
 4     this.$$asyncQueue = []; // scope上的異步隊列
 5     this.$$applyAsyncQueue = []; // scope上的異步apply隊列
 6     this.$$applyAsyncId = null;  //異步apply信息
 7     this.$$postDigestQueue = []; // postDigest執行隊列
 8     this.$$phase = null; // 儲存scope上正在作什麼,值有:digest/apply/null
 9     this.$root = this; // rootScope
10 
11     this.$$listeners = {}; // 存儲包含自定義事件鍵值對的對象
12 
13     this.$$children = []; // 存儲當前scope的兒子Scope,以便$digest循環遞歸
14 }

實際上scope就是一個普通的javascript對象,一個類構造函數,能夠經過new進行實例化。根據髒檢測的原理,接下來,咱們一塊兒看看scope的$watch方法的實現。express

 1 /* $watch方法:向watchers數組中添加watcher對象,以便對應調用 */
 2 Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) {
 3     var self = this;
 4 
 5     watchFn = $parse(watchFn);
 6 
 7     // watchDelegate: 針對watch expression是常量和 one-time-binding的狀況,進行優化。在第一次初始化以後刪除watch
 8     if(watchFn.$$watchDelegate) {
 9         return watchFn.$$watchDelegate(self, listenerFn, valueEq, watchFn);
10     }
11     var watcher = {
12         watchFn: watchFn,
13         listenerFn: listenerFn || function() {},
14         valueEq: !!valueEq,
15         last: initWatchVal
16     };
17 
18     this.$$watchers.unshift(watcher);
19     this.$root.$$lastDirtyWatch = null;
20 
21     return function() {
22         var index = self.$$watchers.indexOf(watcher);
23         if(index >= 0) {
24             self.$$watchers.splice(index, 1);
25             self.$root.$$lastDirtyWatch = null;
26         }
27     };
28 };        

$watch方法的參數:數組

  watchFn-監視表達式,在使用$watch時,一般是傳入一個expression, 通過$parse服務處理後返回一個監視函數,提供動態訪問scope上屬性值的功能,能夠看做 function() { return scope.someValue; }。瀏覽器

  listenerFn-監聽函數,當$digest循環dirty時(即scope上$$watchers數組中有watcher監測到屬性值變化時),執行的回調函數。緩存

  valueEq-是否全等監視,布爾值,valueEq默認爲false,此時$watch對監視對象進行「引用監視」,若是被監視的表達式是原始數據類型,$watch可以發現改變。若是被監視的表達式是引用類型,因爲引用類型的賦值只是將被賦值變量指向當前引用,故$watch認爲沒有改變。若須要對引用類型進行監視,則須要將valueEq設置爲true,這是$watch會對被監視對象進行「全等監視」,在每次比較前會用angular.copy()對被監視對象進行深拷貝,而後用angular.equal()進行比對。雖然「全等監視」可以監視到全部改變,但若是被監視對象很大,性能確定會大打折扣。因此應該根據實際狀況來使用valueEq。app

從代碼中可以看出,$watch的功能其實很是簡單,就是構造watcher對象,並將watcher對象插入到scope.$$watchers數組中,而後返回一個銷燬當前watcher的函數。框架

 

接下來進入到髒檢測最核心的部分:$digest循環

  《Build your own AngularJs》的做者將$digest分紅了兩個函數:$digestOnce 和 $digest。這雖然不用與框架源碼,但可以使代碼更易理解。兩個函數實際上分別對應了$digest的內層循環和外層循環。代碼以下:

內層循環

 1 Scope.prototype.$$digestOnce = function() {
 2             var dirty;
 3             var continueLoop =  true;
 4             var self = this;
 5 
 6             this.$$everyScope(function(scope) {
 7                 var newValue, oldValue;
 8 
 9                 _.forEachRight(scope.$$watchers, function(watcher) {
10                     try {
11                         if(watcher) {
12                             newValue = watcher.watchFn(scope);
13                             oldValue = watcher.last;
14 
15                             if(!scope.$$areEqual(newValue, oldValue, watcher.valueEq)) {
16                                 scope.$root.$$lastDirtyWatch = watcher;
17 
18                                 watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue);
19                                 
20                                 watcher.listenerFn(newValue,
21                                     (oldValue === initWatchVal? newValue : oldValue), scope);
22                                 dirty = true;
23                             } else if(scope.$root.$$lastDirtyWatch === watcher) {
24                                 continueLoop = false;
25                                 return false;
26                             }
27                         }
28                     } catch(e) {
29                         console.error(e);
30                     }
31                 });
32                 return continueLoop;
33             });
34 
35             return dirty;
36         };

  代碼中,$$everyScope是遞歸childScope執行回調函數的工具方法,後面會貼出。

  $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進行髒檢測 以及 對其餘一些異步操做的處理組成。代碼以下:

 1 // digest循環的外循環,保持循環直到沒有髒值爲止
 2         Scope.prototype.$digest = function() {
 3             var ttl = TTL;
 4             var dirty;
 5             this.$root.$$lastDirtyWatch = null;
 6 
 7             this.$beginPhase('$digest');
 8 
 9             if(this.$root.$$applyAsyncId) {
10                 clearTimeout(this.$root.$$applyAsyncId);
11                 this.$$flushApplyAsync();
12             }
13 
14             do {
15                 while (this.$$asyncQueue.length) {
16                     try {
17                         var asyncTask = this.$$asyncQueue.shift();
18                         asyncTask.scope.$eval(asyncTask.expression);
19                     } catch(e) {
20                         console.error(e);
21                     }
22                 }
23 
24                 dirty = this.$$digestOnce();
25 
26                 if((dirty || this.$$asyncQueue.length) && !(ttl--)) {
27                     this.$clearPhase();
28                     throw TTL + ' digest iterations reached';
29                 }
30             } while (dirty || this.$$asyncQueue.length);
31             this.$clearPhase();
32 
33             while(this.$$postDigestQueue.length) {
34                 try {
35                     this.$$postDigestQueue.shift()();
36                 } catch(e) {
37                     console.error(e);
38                 }
39             }
40         };

  在這一節中咱們的主要關注點是髒檢測,異步任務相關的$$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中的屬性)。

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

  

  $apply

  $apply 方法接收一個expression或者function做爲參數,$apply經過$eval函數執行傳入的expression 或 function。最終從$rootScope上觸發$digest循環。

  $apply 被認爲是 使AngularJs與第三方庫混合使用最標準的方式。初學者朋友剛開始都會遇到用第三方庫修改了scope上的屬性或者被watch的屬性,但並無觸發$digest循環,致使雙向綁定失效的問題。此時,$apply就是解決這種狀況的良藥!

1 Scope.prototype.$apply = function(expr) {
2     try {
3         this.$beginPhase('$apply');
4         return this.$eval(expr);
5     } finally {
6         this.$clearPhase();
7         this.$root.$digest();
8     }
9 };    

  $apply本質上,就是用$eval執行了一段表達式,再調用rootScope的$digest方法。

  有時候,當咱們可以肯定咱們不須要從rootScope開始進行$digest循環時,我能夠調用scope.digest() 來代替 $apply,這樣可以帶來性能的提高。

 $evalAsync

  $evalAsync 用於延遲執行一段表達式。一般咱們更習慣使用$timeout服務來進行代碼的延遲執行,但$timeout會將執行控制權交給瀏覽器,若是瀏覽器同時還須要執行諸如 ui渲染/事件控制/ajax 等任務時,咱們代碼延遲執行的時機就會變得很是不可控。

  咱們來看看$evalAsync是如何讓代碼延遲執行的時機變得嚴格,可控的。

 1 Scope.prototype.$evalAsync = function(expr) {
 2     var self = this;
 3     if(!self.$$phase && !self.$$asyncQueue.length) {
 4         setTimeout(function() {
 5             if(self.$$asyncQueue.length) {
 6                 self.$root.$digest();
 7             }
 8         }, 0);
 9     }
10 
11     this.$$asyncQueue.push({
12         scope: this,
13         expression: expr
14     });
15 };

  $evalAsync方法的主要功能是從代碼第11行開始,向$$asyncQueeu中添加對象。$$asyncQueue隊列的執行是在$digest的do while循環中進行的。

while (this.$$asyncQueue.length) {
    try {
        var asyncTask = this.$$asyncQueue.shift();
        asyncTask.scope.$eval(asyncTask.expression);
    } catch(e) {
        console.error(e);
    }
}

  $evalAsync的代碼會在正在運行的$digest循環中被執行,若是當前沒有正在運行的$digest循環,會本身延遲觸發一個$digest循環來執行延遲代碼。

 $applyAsync

  $applyAsync用於合併短期內屢次$digest循環,優化應用性能。

  在平常開發工做中,經常會遇到要短期內接收若干http響應,同時觸發屢次$digest循環的狀況。使用$applyAsync可合併若干次$digest,優化性能。

/* 這個方法用於 知道須要在短期內屢次使用$apply的狀況,
    可以對短期內屢次$digest循環進行合併,
    是針對$digest循環的優化策略
    */
Scope.prototype.$applyAsync = function(expr) {
    var self = this;
    self.$$applyAsyncQueue.push(function() {
        self.$eval(expr);
    });

    if(self.$root.$$applyAsyncId === null) {
        self.$root.$$applyAsyncId = setTimeout(function() {
            self.$apply(_.bind(self.$$flushApplyAsync, self));
        }, 0);
    }
};

 

  $$postDigest

  $$postDigest方法提供了在下一次digest循環後執行代碼的方式,這個方法的前綴是"$$",是一個AngularJs內部方法,應用開發極少用到。

  此方法不自主觸發$digest循環,而是在別處產生$digest循環以後執行。

1 /* $$postDigest 用於在下一次digest循環後執行函數隊列 
2     不一樣於applyAsync 和 evalAsync, 它不觸發digest循環
3     */
4 Scope.prototype.$$postDigest =  function(fn) {
5     this.$$postDigestQueue.push(fn);
6 };

到這裏,咱們對髒檢測的原理,即它的工做機制就瞭解的差很少了。但願這些知識可以幫助你更好的應用AngularJs來開發,可以更輕鬆地定位錯誤。

下一章,我會繼續爲你們介紹文章開頭提到的另外兩處scope相關的特性。篇幅較長,感謝您的耐心閱讀~

相關文章
相關標籤/搜索