Angular.js之手動呼叫$apply()以及$digest()的理解
我希望通過這篇文章讓大家瞭解$apply()和$digest()。最重要的是AngularJS是否能檢測到你對於model的修改。如果它不能檢測到,那麼你就需要手動地呼叫$apply()。
探索$apply()和$digest()
AngularJS提供了一個非常酷的特性叫做雙向資料繫結(Two-way Data Binding),這個特性大大簡化了我們的程式碼編寫方式。資料繫結意味著當View中有任何資料發生了變化,那麼這個變化也會自動地反饋到scope的資料上,也即意味著scope模型會自動地更新。類似地,當scope模型發生變化時,view中的資料也會更新到最新的值。那麼AngularJS
- $scope.$watch('aModel', function(newValue, oldValue) {
- //update the DOM with newValue
- });
傳入到$watch()中的第二個引數是一個回撥函式,該函式在
在$digest迴圈中,watchers會被觸發。當一個watcher被觸發時,AngularJS會檢測scope模型,如何它發生了變化那麼關聯到該watcher的回撥函式就會被呼叫。那麼,下一個問題就是
在呼叫了$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迴圈。
但是,有一個小問題。在上面的例子中,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也會被更新。
Note: $scope.$apply()會自動地呼叫$rootScope.$digest()。$apply()方法有兩種形式。第一種會接受一個function作為引數,執行該function並且觸發一輪$digest迴圈。第二種會不接受任何引數,只是觸發一輪$digest迴圈。我們馬上會看到為什麼第一種形式更好。
什麼時候手動呼叫$apply()方法?
如果AngularJS總是將我們的程式碼wrap到一個function中並傳入$apply(),以此來開始一輪$digest迴圈,那麼什麼時候才需要我們手動地呼叫$apply()方法呢?實際上,AngularJS對此有著非常明確的要求,就是它只負責對發生於AngularJS上下文環境中的變更會做出自動地響應(即,在$apply()方法中發生的對於models的更改)。AngularJS的built-in指令就是這樣做的,所以任何的model變更都會被反映到view中。但是,如果你在AngularJS上下文之外的任何地方修改了model,那麼你就需要通過手動呼叫$apply()來通知AngularJS。這就像告訴AngularJS,你修改了一些models,希望AngularJS幫你觸發watchers來做出正確的響應。
比如,如果你使用了中的setTimeout()來更新一個scope model,那麼AngularJS就沒有辦法知道你更改了什麼。這種情況下,呼叫$apply()就是你的責任了,通過呼叫它來觸發一輪$digest迴圈。類似地,如果你有一個指令用來設定一個DOM事件listener並且在該listener中修改了一些models,那麼你也需要通過手動呼叫$apply()來確保變更會被正確的反映到view中。
讓我們來看一個例子。加入你有一個頁面,一旦該頁面載入完畢了,你希望在兩秒鐘之後顯示一條資訊。你的實現可能是下面這個樣子的:
HTML:
[html] view plain copy print?- <bodyng-app="myApp">
- <divng-controller="MessageController">
- Delayed Message: {{message}}
- </div>
- </body>
[javascript] view plain copy print?
- /* 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(),如下所示:
[javascript] view plain copy print?- /* 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(),就像下面這樣:
[javascript] view plain copy print?- $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沒有變化。