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