理解Angular中的$apply()以及$digest()

$apply() 和 $digest() 在 AngularJS 中是兩個核心概念,可是有時候它們又讓人困惑。而爲了瞭解 AngularJS 的工做方式,首先須要瞭解 $apply() 和 $digest() 是如何工做的。這篇文章旨在解釋 $apply() 和 $digest() 是什麼,以及在平常的編碼中如何應用它們。   app

探索 $apply() 和 $digest()   函數

AngularJS 提供了一個很是酷的特性叫作雙向數據綁定 (Two-way Data Binding) ,這個特性大大簡化了咱們的代碼編寫方式。數據綁定意味着當 View 中有任何數據發生了變化,那麼這個變化也會自動地反饋到 scope 的數據上,也即意味着 scope 模型會自動地更新。相似地,當 scope 模型發生變化時, view 中的數據也會更新到最新的值。那麼 AngularJS 是如何作到這一點的呢?當你寫下表達式如 {{ aModel }} 時, AngularJS 在幕後會爲你在 scope 模型上設置一個 watcher ,它用來在數據發生變化的時候更新 view 。這裏的 watcher 和你會在 AngularJS 中設置的 watcher 是同樣的: ui

$scope.$watch('aModel', function(newValue, oldValue) {//update the DOM with newValue
});

傳入到 $watch() 中的第二個參數是一個回調函數,該函數在 aModel 的值發生變化的時候會被調用。當 aModel 發生變化的時候,這個回調函數會被調用來更新 view 這一點不難理解,可是,還存在一個很重要的問題! AngularJS 是如何知道何時要調用這個回調函數呢?換句話說, AngularJS 是如何知曉 aModel 發生了變化,才調用了對應的回調函數呢?它會週期性的運行一個函數來檢查 scope 模型中的數據是否發生了變化嗎?好吧,這就是 $digest 循環的用武之地了。   this

在 $digest 循環中, watchers 會被觸發。當一個 watcher 被觸發時, AngularJS 會檢測 scope 模型,如何它發生了變化那麼關聯到該 watcher 的回調函數就會被調用。那麼,下一個問題就是 $digest 循環是在何時以各類方式開始的?   編碼

在調用了 $scope.$digest() 後, $digest 循環就開始了。假設你在一個 ng-click 指令對應的 handler 函數中更改了 scope 中的一條數據,此時 AngularJS 會自動地經過調用 $digest() 來觸發一輪 $digest 循環。當 $digest 循環開始後,它會觸發每一個 watcher 。這些 watchers 會檢查 scope 中的當前 model 值是否和上一次計算獲得的 model 值不一樣。若是不一樣,那麼對應的回調函數會被執行。調用該函數的結果,就是 view 中的表達式內容 ( 譯註:諸如 {{ aModel }}) 會被更新。除了 ng-click 指令,還有一些其它的 built-in 指令以及服務來讓你更改 models( 好比 ng-model , $timeout 等 ) 和自動觸發一次 $digest 循環。   code

目前爲止還不錯!可是,有一個小問題。在上面的例子中, AngularJS 並不直接調用 $digest() ,而是調用 $scope.$apply() ,後者會調用 $rootScope.$digest() 。所以,一輪 $digest 循環在 $rootScope 開始,隨後會訪問到全部的 children scope 中的 watchers 。   事件

如今,假設你將 ng-click 指令關聯到了一個 button 上,並傳入了一個 function 名到 ng-click 上。當該 button 被點擊時, AngularJS 會將此 function 包裝到一個 wrapping function 中,而後傳入到 $scope.$apply() 。所以,你的 function 會正常被執行,修改 models( 若是須要的話 ) ,此時一輪 $digest 循環也會被觸發,用來確保 view 也會被更新。   ip

Note: $scope.$apply() 會自動地調用 $rootScope.$digest() 。 $apply() 方法有兩種形式。第一種會接受一個 function 做爲參數,執行該 function 而且觸發一輪 $digest 循環。第二種會不接受任何參數,只是觸發一輪 $digest 循環。咱們立刻會看到爲何第一種形式更好。   get

何時手動調用 $apply() 方法?   回調函數

若是 AngularJS 老是將咱們的代碼 wrap 到一個 function 中並傳入 $apply() ,以此來開始一輪 $digest 循環,那麼何時才須要咱們手動地調用 $apply() 方法呢?實際上, AngularJS 對此有着很是明確的要求,就是它只負責對發生於 AngularJS 上下文環境中的變動會作出自動地響應 ( 即,在 $apply() 方法中發生的對於 models 的更改 ) 。 AngularJS 的 built-in 指令就是這樣作的,因此任何的 model 變動都會被反映到 view 中。可是,若是你在 AngularJS 上下文以外的任何地方修改了 model ,那麼你就須要經過手動調用 $apply() 來通知 AngularJS 。這就像告訴 AngularJS ,你修改了一些 models ,但願 AngularJS 幫你觸發 watchers 來作出正確的響應。  

好比,若是你使用了 JavaScript 中的 setTimeout() 來更新一個 scope model ,那麼 AngularJS 就沒有辦法知道你更改了什麼。這種狀況下,調用 $apply() 就是你的責任了,經過調用它來觸發一輪 $digest 循環。相似地,若是你有一個指令用來設置一個 DOM 事件 listener 而且在該 listener 中修改了一些 models ,那麼你也須要經過手動調用 $apply() 來確保變動會被正確的反映到 view 中。  

讓咱們來看一個例子。加入你有一個頁面,一旦該頁面加載完畢了,你但願在兩秒鐘以後顯示一條信息。你的實現多是下面這個樣子的:

<body ng-app="myApp">
<div ng-controller="MessageController">
Delayed Message: {{message}}</div></body>

JavaScript:

/* What happens without an $apply() */

angular.module('myApp',[]).controller('MessageController', function($scope) {
$scope.getMessage = function() {
setTimeout(function() {$scope.message = 'Fetched after 3 seconds';
console.log('message:'+$scope.message);
}, 2000);
}
$scope.getMessage();

});

經過運行這個例子,你會看到過了兩秒鐘以後,控制檯確實會顯示出已經更新的 model ,然而, view 並無更新。緣由也許你已經知道了,就是咱們忘了調用 $apply() 方法。所以,咱們須要修改 getMessage() ,以下所示:

/* What happens with $apply */ angular.module('myApp',[]).controller('MessageController', function($scope) {
$scope.getMessage = function() {
setTimeout(function() {$scope.$apply(function() {//wrapped this within $apply
$scope.message = 'Fetched after 3 seconds'; console.log('message:' + $scope.message);
});
}, 2000);
}
$scope.getMessage();

});

若是你運行了上面的例子,你會看到 view 在兩秒鐘以後也會更新。惟一的變化是咱們的代碼如今被 wrapped 到了 $scope.$apply() 中,它會自動觸發 $rootScope.$digest() ,從而讓 watchers 被觸發用以更新 view 。  

Note: 順便提一下,你應該使用 $timeout service 來代替 setTimeout() ,由於前者會幫你調用 $apply() ,讓你不須要手動地調用它。  

並且,注意在以上的代碼中你也能夠在修改了 model 以後手動調用沒有參數的 $apply() ,就像下面這樣:

$scope.getMessage = function() {
setTimeout(function() {$scope.message = 'Fetched after two seconds';
console.log('message:' + $scope.message);$scope.$apply(); //this triggers a $digest
}, 2000);
};

以上的代碼使用了 $apply() 的第二種形式,也就是沒有參數的形式。須要記住的是你老是應該使用接受一個 function 做爲參數的 $apply() 方法。這是由於當你傳入一個 function 到 $apply() 中的時候,這個 function 會被包裝到一個 try … catch 塊中,因此一旦有異常發生,該異常會被 $exceptionHandler service 處理。  

$digest 循環會運行多少次?  

當一個 $digest 循環運行時, watchers 會被執行來檢查 scope 中的 models 是否發生了變化。若是發生了變化,那麼相應的 listener 函數就會被執行。這涉及到一個重要的問題。若是 listener 函數自己會修改一個 scope model 呢? AngularJS 會怎麼處理這種狀況?  

答案是 $digest 循環不會只運行一次。在當前的一次循環結束後,它會再執行一次循環用來檢查是否有 models 發生了變化。這就是髒檢查 (Dirty Checking) ,它用來處理在 listener 函數被執行時可能引發的 model 變化。所以, $digest 循環會持續運行直到 model 再也不發生變化,或者 $digest 循環的次數達到了 10 次。所以,儘量地不要在listener 函數中修改 model 。  

Note: $digest 循環最少也會運行兩次,即便在 listener 函數中並無改變任何 model 。正如上面討論的那樣,它會多運行一次來確保 models 沒有變化。  

結語

我但願這篇文章解釋清楚了 $apply 和 $digest 。須要記住的最重要的是 AngularJS 是否能檢測到你對於 model 的修改。若是它不能檢測到,那麼你就須要手動地調用 $apply() 。

相關文章
相關標籤/搜索