如果你寫過(guò)angular程序,肯定對(duì)scope.$apply
不會(huì)陌生,表面上,他的作用就是把改變同步綁定到界面上。但是它為什么存在呢?我們什么時(shí)候需要用到它呢?什么時(shí)候不需要呢?
要真正理解$apply, 就必須知道我們?yōu)槭裁葱枰?/p>
首先,javascript是單線程執(zhí)行的,我們寫的javascript代碼不是一口氣就執(zhí)行完的,而且是很多周期中完成的。每一個(gè)周期從開始到結(jié)束是不會(huì)被中斷的,當(dāng)瀏覽器主線程的一個(gè)周期在執(zhí)行一段代碼時(shí),其他的代碼就不會(huì)執(zhí)行,UI渲染進(jìn)程也會(huì)被掛起,瀏覽器就不會(huì)干被的事,處于一中凍結(jié)狀態(tài),所以糟糕的javascript能將瀏覽器掛起。這就是為什么《高性能javascript》書中說(shuō)到的“一個(gè)函數(shù)執(zhí)行的時(shí)間絕對(duì)不能超過(guò)100ms”的原因,當(dāng)然WebWorker
另當(dāng)別論,WebWorker
是瀏覽器主線程之外的一個(gè)線程,它不是阻塞的,但WebWorker
是不能操作dom節(jié)點(diǎn)的。
當(dāng)運(yùn)行耗時(shí)的周期時(shí),像ajax請(qǐng)求,等待點(diǎn)擊事件,或者是設(shè)置延時(shí),通過(guò)設(shè)置回調(diào)函數(shù)結(jié)束當(dāng)前的運(yùn)行的周期,網(wǎng)絡(luò)請(qǐng)求,用戶輸入,定時(shí)等會(huì)交給底層的操作系統(tǒng),當(dāng)Ajax請(qǐng)求完成時(shí),點(diǎn)擊被觸發(fā),或者是延時(shí)結(jié)束時(shí),一個(gè)新的javascript運(yùn)行周期被創(chuàng)建,回調(diào)函數(shù)的內(nèi)容被執(zhí)行,這個(gè)過(guò)程和操作系統(tǒng)底層 cpu進(jìn)程的運(yùn)行邏輯是一致的。
上面所說(shuō)的周期在javascript里就是Event Loop,對(duì)應(yīng)了一個(gè)事件隊(duì)列,回調(diào)事件,用戶輸入事件等都會(huì)放入隊(duì)列。dom渲染也是事件驅(qū)動(dòng)的,當(dāng)然也就有渲染事件,現(xiàn)在大部分瀏覽器的渲染事件隊(duì)列和javascript事件隊(duì)列使用的是同一個(gè)事件隊(duì)列,這就意味著渲染和執(zhí)行是不會(huì)同時(shí)進(jìn)行的。
angular的模板引擎允許我們把變量綁定到html模板中,而且還做到雙向的綁定,它是如何做到的呢?angular怎么知道我們的變量改變了?angular又是怎么知道何時(shí)應(yīng)該更新dom的呢?
先來(lái)說(shuō)說(shuō),angular是怎么知道變量發(fā)生了改變。
要知道一個(gè)變量變了,方法不外乎兩種:
我們先說(shuō)這個(gè)好處,好處其實(shí)是給我們了,我們的任何對(duì)象都可以綁定,而且是可以隨意賦值。
angualar不會(huì)臟檢查所有的對(duì)象,當(dāng)對(duì)象被綁定到html中,這個(gè)對(duì)象添加為檢查對(duì)象(watcher)。
angular不會(huì)臟檢查所有的屬性,同樣當(dāng)屬性被綁定后,這個(gè)屬性會(huì)被列為檢查的屬性。
我們可以結(jié)合源代碼,看看大概的過(guò)程。下面是watcher的定義:
watcher = {
fn: listener, //監(jiān)聽回調(diào)函數(shù)
last: initWatchVal, //上一狀態(tài)值
get: get, //取得監(jiān)聽的值
exp: watchExp, //監(jiān)聽表達(dá)式
eq: !!objectEquality //要不要比較引用
};
在angular程序初始化時(shí),會(huì)將綁定的對(duì)象的屬性添加為監(jiān)聽對(duì)象(watcher),也就是說(shuō)一個(gè)對(duì)象綁定了N個(gè)屬性,就會(huì)添加N個(gè)watcher。
angular在我們所寫的絕大部分代碼中都會(huì)觸發(fā)比較事件。比如:controller
初始化的時(shí)候,ng-click
事件和所有以ng-
開頭的事件執(zhí)行后,$http
回調(diào)完成后,都會(huì)觸發(fā)臟檢查。
當(dāng)然,觸發(fā)臟檢查的點(diǎn)實(shí)在函數(shù)執(zhí)行完之后,但不表明異步調(diào)用也執(zhí)行完成,所以,如果我們的功能是異步的,那你會(huì)發(fā)現(xiàn)我們的改變并沒有更新到dom上。
來(lái)個(gè)demo?好,請(qǐng)看:
function Ctrl($scope) {
$scope.message = "Waiting 2000ms for update";
setTimeout(function () {
$scope.message = "Timeout called!";
// AngularJS unaware of update to $scope
}, 2000);
}
dom上顯示的message是 “Waiting 2000ms for update,永遠(yuǎn)都不是 “Timeout called!”
這就是$apply
的應(yīng)用場(chǎng)景,使用$scope.$apply()
手動(dòng)觸發(fā)臟檢查。其實(shí)angular還貼心的提供了一個(gè)$timeout
,那$timeout
和setTimeout
在功能上有什么區(qū)別嗎?可以說(shuō)沒有,唯一的區(qū)別就是,使用$timeout
異步完成之后,angular會(huì)自動(dòng)觸發(fā)$apply()
。
如果已經(jīng)在一個(gè)具有$apply
環(huán)境中,調(diào)用$apply()
會(huì)拋出異常, 有空你可以試一試。
所以,如果我們的執(zhí)行代碼不在$apply
環(huán)境中,比如我們查詢sqlite的記錄,結(jié)構(gòu)是異步返回的,就需要用手動(dòng)觸發(fā)$apply()
。
$apply()
接受一個(gè)function的參數(shù),function中被綁定的對(duì)象會(huì)被臟檢查,function不能是異步的哦,這個(gè)很好理解,如果讓你去實(shí)現(xiàn)這個(gè)apply函數(shù),$apply()不帶參數(shù)時(shí),會(huì)將當(dāng)前作用域的所有監(jiān)聽對(duì)象都臟檢查一遍,所以不帶參數(shù)是有害的, 必然會(huì)做很多無(wú)用的臟檢查,浪費(fèi)性能。
臟檢查只要是由$digest()
完成,$apply()
被調(diào)用之后,最終會(huì)觸發(fā)$digest()
,$digest 主要代碼如下:
if ((watchers = current.$watchers)) {
// process our watches
length = watchers.length;
while (length--) {
try {
watch = watchers[length];
// Most common watches are on primitives, in which case we can short
// circuit it with === operator, only when === fails do we use .equals
if (watch
&& (value = watch.get(current)) !== (last = watch.last)
&& !(watch.eq ? equals(value, last) : (typeof value == 'number'
&& typeof last == 'number' && isNaN(value) && isNaN(last)))) {
dirty = true;
watch.last = watch.eq ? copy(value) : value;
watch.fn(value, ((last === initWatchVal) ? value : last), current);
if (ttl < 5) {
logIdx = 4 - ttl;
if (!watchLog[logIdx]) {
watchLog[logIdx] = [];
}
logMsg = (isFunction(watch.exp)) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp;
logMsg += '; newVal: ' + toJson(value) + ';oldVal: ' + toJson(last);
watchLog[logIdx].push(logMsg);
}
}
} catch (e) {
$exceptionHandler(e);
}
}
}
遍歷watchers,比較監(jiān)視的屬性是否變化。
更多建議: