Angular學(xué)習(xí)筆記(15)—digest循環(huán)和$apply

在標(biāo)準(zhǔn)的瀏覽器流程中,當(dāng)事件被觸發(fā)時(shí),瀏覽器會(huì)執(zhí)行注冊(cè)給該事件的回調(diào)函數(shù)。頁面加載、$http請(qǐng)求返回響應(yīng)、鼠標(biāo)移動(dòng)以及按鈕被點(diǎn)擊等情況都會(huì)觸發(fā)事件。
當(dāng)事件被觸發(fā)時(shí),JS就會(huì)創(chuàng)建一個(gè)事件對(duì)象,并執(zhí)行這個(gè)事件對(duì)象所在的監(jiān)聽特定事件的所有函數(shù)。然后它會(huì)運(yùn)行JS函數(shù)內(nèi)的回調(diào)方法,這會(huì)回到瀏覽器中,還可能更新DOM。
同一時(shí)間不能運(yùn)行兩個(gè)事件。瀏覽器會(huì)等待前一個(gè)事件處理程序執(zhí)行完成,再調(diào)用下一個(gè)事件處理程序。
在非Angular JS環(huán)境中,可以給div的點(diǎn)擊事件附加一個(gè)回調(diào)函數(shù)。只要發(fā)現(xiàn)元素上的點(diǎn)擊事件,這個(gè)回調(diào)函數(shù)就會(huì)運(yùn)行。

var div = document.getElementById("clickDiv");
div.addEventListener("click", function(evt) {
    console.log("evt", evt);
});

無論何時(shí),只要瀏覽器檢測(cè)到點(diǎn)擊事件,就會(huì)調(diào)用使用addEventListener注冊(cè)到文檔上的函數(shù)。
當(dāng)我們將Angular混入這個(gè)流程中時(shí),它會(huì)擴(kuò)展這個(gè)標(biāo)準(zhǔn)的瀏覽器流程,創(chuàng)建一個(gè)Angular上下文。這個(gè)Angular上下文指的是運(yùn)行在Angular事件循環(huán)內(nèi)的特定代碼,該Angular事件循環(huán)通常被稱作$digest循環(huán)。$digest循環(huán)有兩個(gè)主要組成部分:

  • $watch列表
  • $evalAsync列表

$watch列表

每當(dāng)我們?cè)谝晥D中追蹤一個(gè)事件時(shí),會(huì)給它注冊(cè)一個(gè)回調(diào)函數(shù),然后希望在觸發(fā)該事件時(shí)調(diào)用這個(gè)回調(diào)函數(shù)。

<input ng-model="name" type="text" placeholder="Your name">
<h1>Hello {{ name }}</h1>

無論何時(shí),只要用戶更新這個(gè)輸入字段,{{name}}就會(huì)改變。發(fā)生這一變化是因?yàn)槲覀儼演斎胱侄谓壎ńo了$scope.name屬性。為了更新這個(gè)視圖,Angular需要追蹤變化。它是通過給$watch列表添加一個(gè)監(jiān)控函數(shù)做到這一點(diǎn)的。
$scope對(duì)象上的屬性只會(huì)在其被用于視圖時(shí)綁定。對(duì)于所有綁定給同一$scope對(duì)象的UI元素,只會(huì)添加一個(gè)$watch$watch列表中。這些$watch列表會(huì)在$digest循環(huán)中通過一個(gè)叫做“臟值檢查”的程序解析。

臟值檢查

臟值檢查可歸結(jié)為一個(gè)非?;A(chǔ)的概念:檢查值是否發(fā)生了變化,而整個(gè)應(yīng)用還沒同步該變化。
Angular應(yīng)用持續(xù)跟蹤當(dāng)前監(jiān)控的值。Angular會(huì)遍歷$watch列表,如果從舊值更新后的值沒有發(fā)生變化,它會(huì)繼續(xù)遍歷監(jiān)控列表。如果值發(fā)生了變化,該應(yīng)用會(huì)啟用新值并繼續(xù)遍歷$watch列表。
Anguar遍歷完整個(gè)$watch列表,只要有任何值發(fā)生變化,應(yīng)用將會(huì)退回到$watch循環(huán)中,直到檢測(cè)到不再有任何變化。
為什么要再次運(yùn)行這一循環(huán)?因?yàn)槿绻愀铝?code>$watch列表中某個(gè)用于更新另一個(gè)值的值,Angular將檢測(cè)不到更新,除非再次運(yùn)行這個(gè)循環(huán)。
如果這個(gè)循環(huán)運(yùn)行10次或者更多次,Angular應(yīng)用會(huì)拋出一個(gè)異常,同時(shí)停止運(yùn)行。如果Angular沒有拋出這個(gè)異常,應(yīng)用就可能進(jìn)入無限循環(huán)。

$watch

$scope對(duì)象上的$watch方法會(huì)給Angular事件循環(huán)內(nèi)的每個(gè)$digest調(diào)用裝配一個(gè)臟值檢查。如果在表達(dá)式上檢測(cè)到變化,Angular總是會(huì)返回$digest循環(huán)。
$watch函數(shù)本身接受兩個(gè)必要參數(shù)和一個(gè)可選的參數(shù):

  • watchExpression
    watchExpression可以是一個(gè)作用域?qū)ο蟮膶傩?,或者是一個(gè)函數(shù)。在$digest循環(huán)中的每個(gè)$digest調(diào)用都會(huì)涉及它。
    如果watchExpression是一個(gè)字符串,Angular會(huì)在$scope上下文中對(duì)它求值。如果它是一個(gè)函數(shù),那么Angular會(huì)認(rèn)為它會(huì)返回應(yīng)該被監(jiān)控的值。
  • listener/callback
    作為回調(diào)的監(jiān)聽器函數(shù),它只會(huì)在watchExpression的當(dāng)前值與先前值不相等(除了首次運(yùn)行初始化期間)時(shí)調(diào)用。
  • objectEquality(可選)
    objectEquality是一個(gè)進(jìn)行比較的布爾值,用來告訴Angular是否檢查嚴(yán)格相等。

$watch函數(shù)會(huì)給監(jiān)聽器返回一個(gè)注銷函數(shù),我們可以調(diào)用這個(gè)注銷函數(shù)來取消Angular對(duì)當(dāng)前值的監(jiān)控。

//...
var unregisterWatch=$scope.$watch('newUser.email',function(newVal,oldVal){
    if (newVal === oldVal) return; // 初始化
});
// 稍后,可以通過調(diào)用這個(gè)注銷函數(shù)來注銷這個(gè)監(jiān)控器
unregisterWatch();

假如完成了對(duì)newUser.email的監(jiān)控,那么可以通過調(diào)用它所返回的注銷函數(shù)來清除這個(gè)監(jiān)控器。
例如,你想要解析一個(gè)輸入字段的值,然后使用空格分割全名的方式找到名字和姓氏。假定給定的視圖看起來像這樣:

<input type="text" ng-model="full_name" placeholder="Enter your full name"/>

我們?cè)?code>full_name屬性上設(shè)置一個(gè)$watch監(jiān)聽器來檢測(cè)值的任意變化。

angular.module("myApp").controller("MyController",['$scope',function($scope){
    $scope.$watch('full_name', function(newVal, oldVal, scope) {
        // newVal表示在這里可以用的full_name新值
        // 而oldVal表示full_name的舊值
    });
}]);

監(jiān)聽函數(shù)會(huì)在初始化時(shí)被調(diào)用一次,而此時(shí)newValoldVal的值都是undefined(并且是相等的)。在這種情況下,如果正處在初始化階段或者先前的值發(fā)生了變化,通常最好是檢查內(nèi)部的表達(dá)式。在監(jiān)控函數(shù)內(nèi)很容易實(shí)現(xiàn)這一檢查。

$scope.$watch('full_name',function(newVal,oldVal,scope) {
    if(newVal === oldVal) {
        // 只會(huì)在監(jiān)控器初始化階段運(yùn)行
    } else {
        // 初始化之后發(fā)生的變化
    }
});

在這段代碼中,$scope.$watch()函數(shù)在$scope對(duì)象上為full_name屬性設(shè)置了一個(gè)監(jiān)控表達(dá)式。

$watchCollection

此外,Angular還允許我們?yōu)閷?duì)象的屬性或者數(shù)組的元素設(shè)置淺監(jiān)控,然后只要屬性發(fā)生變化就觸發(fā)監(jiān)聽器回調(diào)。
使用$watchCollection還可以檢測(cè)對(duì)象或數(shù)組何時(shí)發(fā)生了變化,以便確定對(duì)象或數(shù)組中的條目是何時(shí)添加、移除或者移動(dòng)的。$watchConllection的行為與$digest循環(huán)中標(biāo)準(zhǔn)的$watch的行為一樣,我們甚至可以把它當(dāng)作標(biāo)準(zhǔn)的$watch。
$watchCollectiion()函數(shù)接受2個(gè)參數(shù)。

  • obj(字符串/函數(shù))
    這個(gè)對(duì)象就是一個(gè)要監(jiān)控的對(duì)象。如果傳入一個(gè)字符串,它將被當(dāng)作Angular表達(dá)式求值。
    如果傳入的是一個(gè)函數(shù),將在當(dāng)前作用域中被調(diào)用,并且會(huì)返回要監(jiān)控的值。
  • listener(函數(shù))
    這個(gè)回調(diào)函數(shù)會(huì)在集合發(fā)生變化時(shí)觸發(fā)。類似于$watch函數(shù),這個(gè)函數(shù)會(huì)被來自$watch的新集合觸發(fā)調(diào)用,而原來的集合(先前集合的副本)以及所在的作用域也隨之生效。

$watchConllection()函數(shù)也返回一個(gè)注銷函數(shù)。調(diào)用這個(gè)注銷函數(shù)時(shí),也會(huì)取消集合上的$watch。

$scope.$watchCollection('names',function(newNames,oldNames,scope) {
    // names集合已經(jīng)發(fā)生了變化
});

頁面中的$digest循環(huán)

首先,假設(shè)有一個(gè)登錄頁面,這個(gè)頁面帶有一個(gè)唯一的用戶名字段,允許用戶使用唯一的表單驗(yàn)證進(jìn)行登錄。

<h2>Sign in</h2>
<input type="text" placeholder="Your name" ng-model="name" ng-minlength="3" />
<input type="submit" ng-click="login()" value="Login" />

這里通過ng-model指令在視圖中綁定了一個(gè)name,Angular會(huì)設(shè)置一個(gè)隱式的監(jiān)控器,將這個(gè)輸入字段的值綁定為當(dāng)前的$scope對(duì)象。當(dāng)用戶輸入一個(gè)字符到表單中時(shí),Angular上下文就會(huì)生效并開始遍歷$$watchers$watch列表)。
在這個(gè)例子中,$watch列表只包含了一個(gè)唯一的元素:$scope.name。由于用戶通過輸入一個(gè)字符改變了輸入字段的值,這個(gè)監(jiān)控函數(shù)就會(huì)在$scope.name綁定上執(zhí)行。在我們退出$digest循環(huán)之前,這一行為會(huì)觸發(fā)在該值(由ng-model綁定)上運(yùn)行的驗(yàn)證和格式化操作。
由于在digest循環(huán)中值發(fā)生了變化,Angular需要再次運(yùn)行這一循環(huán)以確定它沒有改變作用域?qū)ο笊系钠渌怠?br> 為什么要再次運(yùn)行digest循環(huán)?如果有一個(gè)名為$scope.full_name的屬性由$scope.first_name+$scope.last_name組成,那么這些值的任何變化都會(huì)改變$scope.full_name,因此循環(huán)需要再次執(zhí)行以確認(rèn)不再有任何變化了。
因?yàn)檫@里只改變了$scope.name屬性,并沒有改變$scope對(duì)象中的其他任何屬性,所以$digest循環(huán)會(huì)退出,然后瀏覽器會(huì)重繪DOM以刷新視圖。
當(dāng)用戶在輸入字段中輸入他們的名字并點(diǎn)擊提交按鈕時(shí),會(huì)引發(fā)一個(gè)略有不同的流程。
ng-click為DOM元素綁定了瀏覽器原生的click事件。當(dāng)這個(gè)DOM元素收到點(diǎn)擊事件時(shí),ng-click指令會(huì)調(diào)用$scope.$apply(),同時(shí)進(jìn)入$digest循環(huán)。

$evalAsync 列表

$evalAsync()方法是一種在當(dāng)前作用域上調(diào)度表達(dá)式在未來某個(gè)時(shí)刻運(yùn)行的方式。$digest循環(huán)運(yùn)行的第二個(gè)操作是執(zhí)行$$asyncQueue??梢允褂?code>$evalAsync()方法訪問這個(gè)工作隊(duì)列。
$digest循環(huán)期間,貫穿臟值檢查生命周期的每個(gè)循環(huán)之間的隊(duì)列都是空的,這意味著使用$evalAsync來調(diào)用任何函數(shù)都會(huì)發(fā)生兩件事情。

  • 函數(shù)會(huì)在這個(gè)方法被調(diào)用的某個(gè)時(shí)刻之后執(zhí)行。
  • 表達(dá)式求值之后至少會(huì)執(zhí)行一次$digest循環(huán)。

$evalAsync()方法接受一個(gè)唯一參數(shù):expression(字符串/函數(shù))。
這個(gè)表達(dá)式便是我們想要在當(dāng)前作用域上執(zhí)行的東西。如果傳入一個(gè)字符串,Angular將會(huì)在當(dāng)前作用域上使用$eval求值該表達(dá)式。
如果傳入的是一個(gè)函數(shù),Angular將會(huì)使用傳遞給這個(gè)函數(shù)的scope對(duì)象執(zhí)行函數(shù)求值。

$scope.$evalAsync('attribute',function(scope) {
    scope.foo = "Executed"
});

使用$evalAsync時(shí)要注意的一些細(xì)節(jié)。

  • 如果指令直接調(diào)用$evalAsync(),它會(huì)在Angular操作DOM之后、瀏覽器渲染之前運(yùn)行。
  • 如果控制器調(diào)用$evalAsync(),它也會(huì)在Angular操作DOM之后、瀏覽器渲染之前運(yùn)行(永遠(yuǎn)不要使用$evalAsync()來約定事件的順序)。

無論何時(shí),在Angular中,只要你想要在一個(gè)行為的執(zhí)行上下文外部執(zhí)行另一個(gè)行為,就應(yīng)該使用$evalAsync()函數(shù)。
你還可以使用它替代setTimeout()函數(shù),但是它可能在瀏覽器重新渲染視圖之后導(dǎo)致屏幕閃爍。

$apply

$apply()函數(shù)可以從Angular框架的外部讓表達(dá)式在Angular上下文內(nèi)部執(zhí)行。例如,假設(shè)你實(shí)現(xiàn)了一個(gè)setTimeout()或者使用第三方庫并且想讓事件運(yùn)行在Angular上下文內(nèi)部時(shí),就必須使用$apply()。
$apply()函數(shù)接受一個(gè)可選的參數(shù):expression(字符串/函數(shù))。
這個(gè)表達(dá)式可選地接受一個(gè)字符串或函數(shù),并且是在當(dāng)前作用域內(nèi)執(zhí)行。
如果傳入一個(gè)字符串,$apply()首先會(huì)在這個(gè)字符串上調(diào)用$eval(),以強(qiáng)制Angular在局部作用域上下文中使用$eval()運(yùn)行字符串表達(dá)式。
如果傳入一個(gè)函數(shù),這個(gè)函數(shù)將會(huì)在所傳入的函數(shù)作用域上執(zhí)行。
$exceptionHandler服務(wù)會(huì)捕獲和處理$eval()方法拋出的所有異常。最后,$apply()方法還會(huì)直接調(diào)用$digest循環(huán)。

// 使用要eval的字符串調(diào)用$apply示例
$scope.$apply('message = "Hello World"');
// 使用函數(shù)的方式并給函數(shù)傳入一個(gè)作用域
$scope.apply(function(scope) {
// 然后在函數(shù)中使用傳入作用域
scope.message = "Hello World";
});
// 使用函數(shù)時(shí)忽略作用域
$scope.$apply(function() {
    $scope.message = "Hello World";
});
// 或者通過在操作的尾部調(diào)用$apply()以強(qiáng)制運(yùn)行$digest循環(huán)
$scope.apply();

簡(jiǎn)而言之,使用$scope.$apply()時(shí)可以從外部進(jìn)入上下文。
如果在事件被觸發(fā)時(shí)調(diào)用$apply(),就會(huì)使用Angular事件循環(huán)來運(yùn)行它。如果沒有調(diào)用$apply(),就不會(huì)在事件循環(huán)內(nèi)執(zhí)行這個(gè)函數(shù),而它會(huì)運(yùn)行在Angular上下文外部。

何時(shí)使用$apply

通??梢砸蕾囉贏ngular提供的可用于視圖中的任意指令來調(diào)用$apply()。所有ng-[event]指令(比如ng-clickng-keypress)都會(huì)調(diào)用$apply()。
此外還可以依賴于一系列Angular內(nèi)置的服務(wù)來調(diào)用$digest()。比如$http服務(wù)會(huì)在XHR請(qǐng)求完成并觸發(fā)更新返回值(promise)之后調(diào)用$apply()
無論何時(shí)我們手動(dòng)處理事件,使用第三方框架(比如jQuery),或者調(diào)用setTimeout(),都可以使用$apply()函數(shù)讓Angular返回$digest循環(huán)。
一般不建議在控制器中使用$apply(),因?yàn)檫@樣會(huì)導(dǎo)致難以測(cè)試,而且如果不得不在控制器中使用$apply()或者$digest(),很可能讓事情變得更加難以理解。
當(dāng)我們將jQuery和Angular集成在一起時(shí),就需要使用$apply(),因?yàn)锳ngular不會(huì)察覺到執(zhí)行在Angular上下文外部的事件。例如,在使用jQuery插件時(shí),就需要使用$apply()將來自jQuery的值傳遞到Angular應(yīng)用中。
在這里,我們構(gòu)建了一個(gè)簡(jiǎn)單的指令,指令中我們?cè)谠厣鲜褂昧?code>datepicker這個(gè)jQuery插件方法。datepicker插件暴露了一個(gè)onSelect事件,這個(gè)事件會(huì)在用戶選擇日期時(shí)觸發(fā)。為了在Angular應(yīng)用內(nèi)部獲取用戶選擇的日期,我們需要在$apply()函數(shù)內(nèi)運(yùn)行datepicker的回調(diào)函數(shù)。
ele.datepicker()函數(shù)是由jQuery datepicker插件提供的可用于DOM元素的屬性方法。ctrl.$setViewValue()函數(shù)是在DOM元素上使用ng-model時(shí)提供的指令。

app.directive('myDatepicker',function() {
    return function(scope,ele,attrs,ctrl) {
        $(function() {
            // 在元素上調(diào)用datepicker方法
            ele.datepicker({
                dateFormat: 'mm/dd/yy',
                onSelect: function(date) {
                    scope.$apply(function() {
                        ctrl.$setViewValue(date);
                    });
                }
            });
        });
    }
});
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容