Angular學習筆記(15)—digest循環和$apply

在標準的瀏覽器流程中,當事件被觸發時,瀏覽器會執行注冊給該事件的回調函數。頁面加載、$http請求返回響應、鼠標移動以及按鈕被點擊等情況都會觸發事件。
當事件被觸發時,JS就會創建一個事件對象,并執行這個事件對象所在的監聽特定事件的所有函數。然后它會運行JS函數內的回調方法,這會回到瀏覽器中,還可能更新DOM。
同一時間不能運行兩個事件。瀏覽器會等待前一個事件處理程序執行完成,再調用下一個事件處理程序。
在非Angular JS環境中,可以給div的點擊事件附加一個回調函數。只要發現元素上的點擊事件,這個回調函數就會運行。

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

無論何時,只要瀏覽器檢測到點擊事件,就會調用使用addEventListener注冊到文檔上的函數。
當我們將Angular混入這個流程中時,它會擴展這個標準的瀏覽器流程,創建一個Angular上下文。這個Angular上下文指的是運行在Angular事件循環內的特定代碼,該Angular事件循環通常被稱作$digest循環。$digest循環有兩個主要組成部分:

  • $watch列表
  • $evalAsync列表

$watch列表

每當我們在視圖中追蹤一個事件時,會給它注冊一個回調函數,然后希望在觸發該事件時調用這個回調函數。

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

無論何時,只要用戶更新這個輸入字段,{{name}}就會改變。發生這一變化是因為我們把輸入字段綁定給了$scope.name屬性。為了更新這個視圖,Angular需要追蹤變化。它是通過給$watch列表添加一個監控函數做到這一點的。
$scope對象上的屬性只會在其被用于視圖時綁定。對于所有綁定給同一$scope對象的UI元素,只會添加一個$watch$watch列表中。這些$watch列表會在$digest循環中通過一個叫做“臟值檢查”的程序解析。

臟值檢查

臟值檢查可歸結為一個非?;A的概念:檢查值是否發生了變化,而整個應用還沒同步該變化。
Angular應用持續跟蹤當前監控的值。Angular會遍歷$watch列表,如果從舊值更新后的值沒有發生變化,它會繼續遍歷監控列表。如果值發生了變化,該應用會啟用新值并繼續遍歷$watch列表。
Anguar遍歷完整個$watch列表,只要有任何值發生變化,應用將會退回到$watch循環中,直到檢測到不再有任何變化。
為什么要再次運行這一循環?因為如果你更新了$watch列表中某個用于更新另一個值的值,Angular將檢測不到更新,除非再次運行這個循環。
如果這個循環運行10次或者更多次,Angular應用會拋出一個異常,同時停止運行。如果Angular沒有拋出這個異常,應用就可能進入無限循環。

$watch

$scope對象上的$watch方法會給Angular事件循環內的每個$digest調用裝配一個臟值檢查。如果在表達式上檢測到變化,Angular總是會返回$digest循環。
$watch函數本身接受兩個必要參數和一個可選的參數:

  • watchExpression
    watchExpression可以是一個作用域對象的屬性,或者是一個函數。在$digest循環中的每個$digest調用都會涉及它。
    如果watchExpression是一個字符串,Angular會在$scope上下文中對它求值。如果它是一個函數,那么Angular會認為它會返回應該被監控的值。
  • listener/callback
    作為回調的監聽器函數,它只會在watchExpression的當前值與先前值不相等(除了首次運行初始化期間)時調用。
  • objectEquality(可選)
    objectEquality是一個進行比較的布爾值,用來告訴Angular是否檢查嚴格相等。

$watch函數會給監聽器返回一個注銷函數,我們可以調用這個注銷函數來取消Angular對當前值的監控。

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

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

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

我們在full_name屬性上設置一個$watch監聽器來檢測值的任意變化。

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

監聽函數會在初始化時被調用一次,而此時newValoldVal的值都是undefined(并且是相等的)。在這種情況下,如果正處在初始化階段或者先前的值發生了變化,通常最好是檢查內部的表達式。在監控函數內很容易實現這一檢查。

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

在這段代碼中,$scope.$watch()函數在$scope對象上為full_name屬性設置了一個監控表達式。

$watchCollection

此外,Angular還允許我們為對象的屬性或者數組的元素設置淺監控,然后只要屬性發生變化就觸發監聽器回調。
使用$watchCollection還可以檢測對象或數組何時發生了變化,以便確定對象或數組中的條目是何時添加、移除或者移動的。$watchConllection的行為與$digest循環中標準的$watch的行為一樣,我們甚至可以把它當作標準的$watch
$watchCollectiion()函數接受2個參數。

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

$watchConllection()函數也返回一個注銷函數。調用這個注銷函數時,也會取消集合上的$watch。

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

頁面中的$digest循環

首先,假設有一個登錄頁面,這個頁面帶有一個唯一的用戶名字段,允許用戶使用唯一的表單驗證進行登錄。

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

$evalAsync 列表

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

  • 函數會在這個方法被調用的某個時刻之后執行。
  • 表達式求值之后至少會執行一次$digest循環。

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

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

使用$evalAsync時要注意的一些細節。

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

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

$apply

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

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

簡而言之,使用$scope.$apply()時可以從外部進入上下文。
如果在事件被觸發時調用$apply(),就會使用Angular事件循環來運行它。如果沒有調用$apply(),就不會在事件循環內執行這個函數,而它會運行在Angular上下文外部。

何時使用$apply

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

app.directive('myDatepicker',function() {
    return function(scope,ele,attrs,ctrl) {
        $(function() {
            // 在元素上調用datepicker方法
            ele.datepicker({
                dateFormat: 'mm/dd/yy',
                onSelect: function(date) {
                    scope.$apply(function() {
                        ctrl.$setViewValue(date);
                    });
                }
            });
        });
    }
});
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容