国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > JavaScript > 正文

深入理解AngularJs-scope的臟檢查(一)

2019-11-19 16:18:30
字體:
來源:轉載
供稿:網友

進入正文前的說明:本文中的示例代碼并非AngularJs源碼,而是來自書籍<<Build Your Own AngularJs>>, 這本書的作者僅依賴jquery和lodash一步一步構建出AngularJs的各核心模塊,對全面理解AngularJs有非常巨大的幫助。若有正在使用AngulaJs攻城拔寨并且希望完全掌握手中武器的小伙伴,相信能對你理解AngularJs帶來莫大幫助,感謝作者。

在這篇文章中,希望能讓您理清楚以下幾項與scope相關的功能:

1.dirty-checking(臟檢測)核心機制,主要包括:$watch 和 $digest;

2.幾種不同的觸發$digest循環的方式:$eval, $apply, $evalAsync, $applyAsync;

3.scope的繼承機制以及isolated scope;

4.依賴于scope的事件循環:$on, $broadcast, $emit.

現在開始我們的第一部分:scope和dirty-checking

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

function Scope() {  this.$$watchers = []; // 監聽器數組  this.$$lastDirtyWatch = null; // 每次digest循環的最后一個臟的watcher, 用于優化digest循環  this.$$asyncQueue = []; // scope上的異步隊列  this.$$applyAsyncQueue = []; // scope上的異步apply隊列  this.$$applyAsyncId = null; //異步apply信息  this.$$postDigestQueue = []; // postDigest執行隊列  this.$$phase = null; // 儲存scope上正在做什么,值有:digest/apply/null  this.$root = this; // rootScope  this.$$listeners = {}; // 存儲包含自定義事件鍵值對的對象  this.$$children = []; // 存儲當前scope的兒子Scope,以便$digest循環遞歸}

實際上scope就是一個普通的javascript對象,一個類構造函數,可以通過new進行實例化。根據臟檢測的原理,接下來,我們一起看看scope的$watch方法的實現。

/* $watch方法:向watchers數組中添加watcher對象,以便對應調用 */Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) {  var self = this;  watchFn = $parse(watchFn);  // watchDelegate: 針對watch expression是常量和 one-time-binding的情況,進行優化。在第一次初始化之后刪除watch  if(watchFn.$$watchDelegate) {    return watchFn.$$watchDelegate(self, listenerFn, valueEq, watchFn);  }  var watcher = {    watchFn: watchFn,    listenerFn: listenerFn || function() {},    valueEq: !!valueEq,    last: initWatchVal  };  this.$$watchers.unshift(watcher);  this.$root.$$lastDirtyWatch = null;  return function() {    var index = self.$$watchers.indexOf(watcher);    if(index >= 0) {      self.$$watchers.splice(index, 1);      self.$root.$$lastDirtyWatch = null;    }  };};

$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。

從代碼中能夠看出,$watch的功能其實非常簡單,就是構造watcher對象,并將watcher對象插入到scope.$$watchers數組中,然后返回一個銷毀當前watcher的函數。

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

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

內層循環

Scope.prototype.$$digestOnce = function() {      var dirty;      var continueLoop = true;      var self = this;      this.$$everyScope(function(scope) {        var newValue, oldValue;        _.forEachRight(scope.$$watchers, function(watcher) {          try {            if(watcher) {              newValue = watcher.watchFn(scope);              oldValue = watcher.last;              if(!scope.$$areEqual(newValue, oldValue, watcher.valueEq)) {                scope.$root.$$lastDirtyWatch = watcher;                watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue);                                watcher.listenerFn(newValue,                  (oldValue === initWatchVal? newValue : oldValue), scope);                dirty = true;              } else if(scope.$root.$$lastDirtyWatch === watcher) {                continueLoop = false;                return false;              }            }          } catch(e) {            console.error(e);          }        });        return continueLoop;      });      return dirty;    };

代碼中,$$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進行臟檢測 以及 對其他一些異步操作的處理組成。代碼如下:

// digest循環的外循環,保持循環直到沒有臟值為止    Scope.prototype.$digest = function() {      var ttl = TTL;      var dirty;      this.$root.$$lastDirtyWatch = null;      this.$beginPhase('$digest');      if(this.$root.$$applyAsyncId) {        clearTimeout(this.$root.$$applyAsyncId);        this.$$flushApplyAsync();      }      do {        while (this.$$asyncQueue.length) {          try {            var asyncTask = this.$$asyncQueue.shift();            asyncTask.scope.$eval(asyncTask.expression);          } catch(e) {            console.error(e);          }        }        dirty = this.$$digestOnce();        if((dirty || this.$$asyncQueue.length) && !(ttl--)) {          this.$clearPhase();          throw TTL + ' digest iterations reached';        }      } while (dirty || this.$$asyncQueue.length);      this.$clearPhase();      while(this.$$postDigestQueue.length) {        try {          this.$$postDigestQueue.shift()();        } catch(e) {          console.error(e);        }      }    };

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

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

$apply

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

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

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

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

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

 $evalAsync

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

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

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

$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循環之后執行。

/* $$postDigest 用于在下一次digest循環后執行函數隊列    不同于applyAsync 和 evalAsync, 它不觸發digest循環   */ Scope.prototype.$$postDigest = function(fn) {   this.$$postDigestQueue.push(fn); };

到這里,我們對臟檢測的原理,即它的工作機制就了解的差不多了。希望這些知識能夠幫助你更好的應用AngularJs來開發,能夠更輕松地定位錯誤。

下一章,我會繼續為大家介紹文章開頭提到的另外兩處scope相關的特性。篇幅較長,感謝您的耐心閱讀~也希望大家多多支持武林網。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 家居| 会昌县| 松原市| 抚宁县| 南城县| 缙云县| 高清| 南汇区| 贵德县| 涟源市| 南昌县| 历史| 平定县| 虎林市| 阿拉善右旗| 涞源县| 土默特右旗| 华坪县| 乌苏市| 双桥区| 泰安市| 文安县| 瑞丽市| 广昌县| 宜章县| 湘潭市| 沧州市| 灯塔市| 松潘县| 太仆寺旗| 海阳市| 西畴县| 石楼县| 通榆县| 昆明市| 江源县| 武川县| 桐梓县| 衡山县| 永济市| 衡阳市|