AngularJS的scope.$apply

2018-06-07 18:25 更新

如果你寫(xiě)過(guò)angular程序,肯定對(duì)scope.$apply不會(huì)陌生,表面上,他的作用就是把改變同步綁定到界面上。但是它為什么存在呢?我們什么時(shí)候需要用到它呢?什么時(shí)候不需要呢?

要真正理解$apply, 就必須知道我們?yōu)槭裁葱枰?/p>

首先,javascript是單線程執(zhí)行的,我們寫(xiě)的javascript代碼不是一口氣就執(zhí)行完的,而且是很多周期中完成的。每一個(gè)周期從開(kāi)始到結(jié)束是不會(huì)被中斷的,當(dāng)瀏覽器主線程的一個(gè)周期在執(zhí)行一段代碼時(shí),其他的代碼就不會(huì)執(zhí)行,UI渲染進(jìn)程也會(huì)被掛起,瀏覽器就不會(huì)干被的事,處于一中凍結(jié)狀態(tài),所以糟糕的javascript能將瀏覽器掛起。這就是為什么《高性能javascript》書(shū)中說(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)行的。

AngularJS 如何更新綁定?

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

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

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

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

我們先說(shuō)這個(gè)好處,好處其實(shí)是給我們了,我們的任何對(duì)象都可以綁定,而且是可以隨意賦值。

首先是臟檢查的對(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)聽(tīng)回調(diào)函數(shù)
    last: initWatchVal,    //上一狀態(tài)值
    get: get,              //取得監(jiān)聽(tīng)的值
    exp: watchExp,         //監(jiān)聽(tīng)表達(dá)式
    eq: !!objectEquality   //要不要比較引用
};

在angular程序初始化時(shí),會(huì)將綁定的對(duì)象的屬性添加為監(jiān)聽(tīng)對(duì)象(watcher),也就是說(shuō)一個(gè)對(duì)象綁定了N個(gè)屬性,就會(huì)添加N個(gè)watcher。

其次就是什么時(shí)候去臟檢查

angular在我們所寫(xiě)的絕大部分代碼中都會(huì)觸發(fā)比較事件。比如:controller初始化的時(shí)候,ng-click事件和所有以ng-開(kāi)頭的事件執(zhí)行后,$http回調(diào)完成后,都會(huì)觸發(fā)臟檢查。

當(dāng)然,觸發(fā)臟檢查的點(diǎn)實(shí)在函數(shù)執(zhí)行完之后,但不表明異步調(diào)用也執(zhí)行完成,所以,如果我們的功能是異步的,那你會(huì)發(fā)現(xiàn)我們的改變并沒(méi)有更新到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,那$timeoutsetTimeout在功能上有什么區(qū)別嗎?可以說(shuō)沒(méi)有,唯一的區(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)聽(tīng)對(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)視的屬性是否變化。

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)