放棄不穩(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ā)生了變化。