AngularJS的scope.$apply

2018-06-07 18:25 更新

如果你寫過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í)行是不會同時進行的。

AngularJS 如何更新綁定?

angular的模板引擎允許我們把變量綁定到html模板中,而且還做到雙向的綁定,它是如何做到的呢?angular怎么知道我們的變量改變了?angular又是怎么知道何時應該更新dom的呢?

先來說說,angular是怎么知道變量發(fā)生了改變。

要知道一個變量變了,方法不外乎兩種:

  • 第一種,只能通過固定的接口才能改變變量的值,比如說只能通過 set() 設置變量的值,set被調(diào)用時比較一下就知道了。這中方法的缺點洗是寫法繁瑣,只能用obj.set(‘key’, ‘value’) 替代obj.key = ‘value’。EmberJS 和 KnockoutJS 都使用的是這種策略
  • 第二種, 臟檢查,將原對象復制一份快照,在某個時間,比較現(xiàn)在對象與快照的值,如果不一樣就表明發(fā)生變化。很明顯,這個策略要保留兩份變量,而且要遍歷對象,比較每個屬性,這樣不會有性能問題嗎?但偏偏angular使用的是這樣方式,是google的程序員傻嗎?angular沒有性能問題嗎?它是怎么做的呢?

我們先說這個好處,好處其實是給我們了,我們的任何對象都可以綁定,而且是可以隨意賦值。

首先是臟檢查的對象

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,那$timeoutsetTimeout在功能上有什么區(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)視的屬性是否變化。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號