創(chuàng)建你自己的AngularJS-第一部分 Scopes (2)

放棄不穩(wěn)定的Digest

在我們現(xiàn)在實(shí)現(xiàn)的代碼中有一個(gè)明顯的遺漏問(wèn)題:如果有兩個(gè)監(jiān)視器相互監(jiān)視彼此的變化那么會(huì)發(fā)生什么?換而言之,如果狀態(tài)永遠(yuǎn)是不穩(wěn)定會(huì)是什么樣的情況?下面的代碼將呈現(xiàn)這種情形:

test/scope_spec.js


it("gives up on the watches after 10 iterations",function(){
  scope.counterA = 0;
  scope.counterB = 0;

  scope.$watch(
    function(scope){ return scope.counterA },
    function(newValue,oldValue,scope){
      scope.counterB++;
    }
  );

  scope.$watch(
    function(scope){ retrun scope.counterB; },
    function(newValue,oldValue,scope){
      scope.counterA++;
    }
  );
  expect((function(){ scope.$digest(); })).toThrow();
});

我們期望scope.$digest拋出一個(gè)異常,但他并沒(méi)有。事實(shí)上這個(gè)測(cè)試永遠(yuǎn)不會(huì)結(jié)束。這是因?yàn)檫@兩個(gè)計(jì)數(shù)器相互依賴,以至于在每一次的迭代它們當(dāng)中的$$digestOnce總有一個(gè)是dirty的。

注意我們并沒(méi)有直接調(diào)用scope.$digest函數(shù),而是使用Jasmine的excpect函數(shù),它會(huì)替我們調(diào)用該函數(shù),以便它能夠檢查是否拋出一個(gè)與我們預(yù)期一樣的異常。
由于測(cè)試用例將會(huì)一直運(yùn)行,你需要終止測(cè)試進(jìn)程來(lái)結(jié)束測(cè)試用例并當(dāng)我們修復(fù)這個(gè)問(wèn)題后再重新運(yùn)行。

我們需要做的就是給digest一個(gè)可循環(huán)的次數(shù)。如果超過(guò)這個(gè)次數(shù)scope還一直在在改變那么我們就退出循環(huán)并聲明它可能永遠(yuǎn)不會(huì)結(jié)束,在這一點(diǎn)上我們可以會(huì)拋出一個(gè)異常,因?yàn)閟cope的這種狀態(tài)可能并不是用戶想要的。

最大數(shù)量的迭代就叫做TTL("Time To Live"的縮寫(xiě))。默認(rèn)設(shè)置為10。這個(gè)次數(shù)看起來(lái)可能有點(diǎn)小,但是考慮到這是影響性能的敏感區(qū),因?yàn)閐igests的次數(shù)繁多而且每次digest都會(huì)運(yùn)行所有監(jiān)視函數(shù)。況且一個(gè)用戶有超過(guò)10個(gè)監(jiān)視器緊密鏈接的情況也是不太可能的。

實(shí)際上在Angular中TTL是可以調(diào)整的。當(dāng)我們討論到了providers和依賴注入時(shí),我們將再進(jìn)行詳細(xì)說(shuō)明。

讓我們繼續(xù)并添加一個(gè)循環(huán)計(jì)數(shù)器到我們的外部digest循環(huán)中。如果它到達(dá)TTL,我們將拋出一個(gè)異常:

src/scope.js


Scope.prototype.$digest = function(){
  var ttl = 10;
  var dirty;
  do{
    dirty = this.$$digestOnce();
    if(dirty && !(ttl--)){
      throw "10 digest iterations reached";
    }
  }while(dirty);
};

這次更新后再運(yùn)行我們相互依賴的監(jiān)視器的例子,會(huì)拋出一個(gè)如我們測(cè)試預(yù)計(jì)一樣的異常。它應(yīng)該為我們結(jié)束digest的運(yùn)行。

Digest的短路(當(dāng)最后一個(gè)監(jiān)視器Clean時(shí)立即結(jié)束Digest)

在目前執(zhí)行情況,我們不停的迭代監(jiān)視器的集合直到我們?cè)谝淮瓮暾难h(huán)里面所有的監(jiān)視器都是clean(或者循環(huán)次數(shù)當(dāng)?shù)竭_(dá)TTL)。

因?yàn)槲覀兛赡茉谝淮蝑igest循環(huán)中擁有大量的監(jiān)視器,盡可能少的執(zhí)行它們就變得尤其重要。這就是為什么我們要為digest循環(huán)使用一個(gè)特定的優(yōu)化。

假設(shè)在一個(gè)scope上有100個(gè)監(jiān)視器。當(dāng)我們digest這個(gè)scope,只有第一個(gè)監(jiān)視器恰好是dirty的。那么這一個(gè)監(jiān)視器使得整個(gè)digest循環(huán)變成了dirty的,而且我們不得不在做一次循環(huán)。在第二次循環(huán)中,沒(méi)有監(jiān)視器是dirty的然后digest結(jié)束。但是在這之前我們必須執(zhí)行200個(gè)監(jiān)視器!

為了使我們的執(zhí)行次數(shù)減少一半,我們可以追蹤在循環(huán)中最后為dirty的監(jiān)視器,然后,每當(dāng)我們遇見(jiàn)一個(gè)clean的監(jiān)視器,我們就檢查是否這個(gè)監(jiān)視器就是我們之前所遇的那個(gè)dirty的監(jiān)視器。如果是,這意味著在一整個(gè)循環(huán)中已經(jīng)沒(méi)有dirty的監(jiān)視器了。在這種情況下當(dāng)前這個(gè)循環(huán)結(jié)束后就沒(méi)有必要在繼續(xù)下一次了。我們可以立即退出。下面是一個(gè)表示這種情況的測(cè)試用例:

test/scope_spec.js


it("ends the digest when the last watch is clean",function(){
  scope.array = _.range(100);
  var watchExecutions = 0;

  _.times(100,function(i){
    scope.$watch(
      function(scope){
        watchExecutions++;
        return scope.array[i];
      },
      function(newValue,oldValue,scope){
      }
    );
  });

  scope.$digest();
  expect(watchExecutions).toBe(200);

  scope.array[0] = 420;
  scope.$digest();
  expect(watchExecutions).toBe(301);
});

我們首先把包含100個(gè)元素的數(shù)組放到scope的array屬性中。然后我們附上100個(gè)監(jiān)視器,每個(gè)監(jiān)視數(shù)組中一個(gè)單獨(dú)的元素。同時(shí)我們也添加一個(gè)本地遞增變量,每當(dāng)一個(gè)監(jiān)視器執(zhí)行時(shí)就執(zhí)行自增操作,這樣我們就可以跟蹤監(jiān)視器執(zhí)行次數(shù)的總數(shù)。

然后為了初始化監(jiān)視器,我們運(yùn)行一次digest。在digest每個(gè)監(jiān)視器將被運(yùn)行兩次。

然后我們改變數(shù)組中的第一項(xiàng)。如果短路優(yōu)化已經(jīng)生效,那么digest將會(huì)在第一個(gè)監(jiān)視器在被執(zhí)行第二次迭代時(shí)立即結(jié)束,最后的執(zhí)行總數(shù)將是301而不是400。

如前面所說(shuō)的,這種優(yōu)化可以通過(guò)跟蹤最后一個(gè)dirty的監(jiān)視器來(lái)實(shí)現(xiàn)。讓我們?cè)赟cope的構(gòu)造器中為其添加一個(gè)字段:

src/scope.js


function Scope(){
  this.$$watchers = [];
  this.$$lastDirtyWatch = null;
}

現(xiàn)在,無(wú)論digest什么時(shí)候開(kāi)始,讓我們把這個(gè)字段設(shè)置為null:

src/scope.js


Scope.prototype.$digest = function(){
  var ttl = 10;
  var dirty;
  this.$$lastDirtyWatch = null;
  do{
    dirty = this.$$digestOnce();
    if(dirty && !(ttl--)){
      throw "10 digest iterations reached";
    }
  } while (dirty);
};

在$$digestOnce中,無(wú)論我們何時(shí)遇到一個(gè)dirty的監(jiān)視器,都把它分配當(dāng)這個(gè)字段當(dāng)中:

src/scope.js


Scope.prototype.$$digestOnce = function(){
  var self = this;
  var newValue,oldValue,dirty;
  _.forEach(this.$$watchers,function(watcher){
    newValue = watcher.watchFn(self);
    oldValue = watcher.last;
    if(newValue !== oldValue){
      self.$$lastDirtyWatch = watcher;
      watcher.last = newValue;
      watcher.listenerFn(newValue,
        (oldValue === initWatchVal ? newValue : oldValue),
        self);
        dirty = true;
    }
  });
  return dirty;
}

同樣在$$digestOnce中,無(wú)論何時(shí)我們碰到一個(gè)clean的監(jiān)視器我們同樣對(duì)它做和最后一個(gè)dirty的監(jiān)視器同樣的操作。讓我們打斷循環(huán)并返回一個(gè)false值,以便讓外部$digest循環(huán)知道它應(yīng)該停止迭代了:

src/scope.js


Scope.prototype.$$digestOnce = function(){
  var self = this;
  var newValue,oldValue,dirty;
  _.forEach(this.$$watchers,function(watcher){
    newValue = watcher.watchFn(self);
    oldValue = watcher.last;
    
    if(newValue !== oldValue){
      self.$$lastDirtyWatch = watcher;
      watcher.last = newValue;
      watcher.listenerFn(newValue,
        (oldValue === initWatchVal ? newValue : oldValue),
         self);
        dirty = true;
    } else if (self.$$lastDirtyWatch === watcher) {
      return false;
    }
  });
  return dirty;
};

由于此時(shí)我們還沒(méi)有遇到任何dirty的監(jiān)視器,dirty將會(huì)是undefined,并且這個(gè)將會(huì)被當(dāng)作這個(gè)函數(shù)的返回值。

在_.forEach循環(huán)中顯示的返回false,將會(huì)造成LoDash的循環(huán)短路,并立即退出。

優(yōu)化現(xiàn)在已經(jīng)生效。在這里還有一種情況我們需要考慮到到,那就是我們可以通過(guò)添加一個(gè)監(jiān)視器來(lái)監(jiān)視另外一個(gè)監(jiān)視器:

test/scope_spec.js


it("does not end digest so that new watches are not run",function(){
  scope.aValue = 'abc';
  scope.counter = 0;

  scope.$watch(
    function(scope) { return scope.aValue; },
    function(newValue,oldValue,scope) {
      scope.$watch(
        function(scope) { return scope.aValue; },
        function(newValue,oldValue,scope){
          scope.counter++;
        }
      );
    }
  );

  scope.$digest();
  expect(scope.counter).toBe(1);
});

第二個(gè)監(jiān)視器沒(méi)有被執(zhí)行。原因是在第二次digest迭代時(shí),運(yùn)行到新監(jiān)視器的前面,我們就結(jié)束了digest因?yàn)槲覀儥z查到第一個(gè)監(jiān)視器就是最后一個(gè)dirty的監(jiān)視器,而現(xiàn)在它是clean的。讓我們通過(guò)當(dāng)一個(gè)監(jiān)視器被添加時(shí),禁用優(yōu)化復(fù)位$$lastDirtyWatch來(lái)修復(fù)這個(gè)問(wèn)題:

src/scope.js


Scope.prototype.$watch = function(watchFn,listenerFn){
  var watcher = {
    watchFn : watchFn,
    listenerFn : listenerFn || function(){ },
    last : initWatchVal
  };
  this.$$watchers.push(watcher);
  this.$$lastDirtyWatch = null;
};

現(xiàn)在我們的digest的周期可能比以前快很多。在一個(gè)典型的應(yīng)用,這迭代優(yōu)化可能并不總是像在我們的例子中一樣有效,但是通常來(lái)說(shuō)它已經(jīng)足夠好了所以Angular團(tuán)隊(duì)才決定使用它。

現(xiàn)在,讓我們把注意力轉(zhuǎn)到我們?cè)鯓硬拍馨l(fā)現(xiàn)哪些東西發(fā)生了變化。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容