angular中的$digest()與$apply()

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

 

探索$apply()$digest()html

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

 

[javascript]  view plain  copy
 
 print?
  1. $scope.$watch('aModel', function(newValue, oldValue) {  
  2.   //update the DOM with newValue  
  3. });  

 

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

 

$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循環。ui

 

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

 

如今,假設你將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循環。咱們立刻會看到爲何第一種形式更好。spa

 

何時手動調用$apply()方法?.net

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

 

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

 

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

 

HTML:

 

[html]  view plain  copy
 
 print?
  1. <body ng-app="myApp">  
  2.   <div ng-controller="MessageController">  
  3.     Delayed Message: {{message}}  
  4.   </div>    
  5. </body>  

 

 

JavaScript:

[javascript]  view plain  copy
 
 print?
  1. /* What happens without an $apply() */  
  2.       
  3.     angular.module('myApp',[]).controller('MessageController', function($scope) {  
  4.       
  5.       $scope.getMessage = function() {  
  6.         setTimeout(function() {  
  7.           $scope.message = 'Fetched after 3 seconds';  
  8.           console.log('message:'+$scope.message);  
  9.         }, 2000);  
  10.       }  
  11.         
  12.       $scope.getMessage();  
  13.       
  14.     });  

 

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

 

 

[javascript]  view plain  copy
 
 print?
  1. /* What happens with $apply */   
  2. angular.module('myApp',[]).controller('MessageController', function($scope) {  
  3.       
  4.       $scope.getMessage = function() {  
  5.         setTimeout(function() {  
  6.           $scope.$apply(function() {  
  7.             //wrapped this within $apply  
  8.             $scope.message = 'Fetched after 3 seconds';   
  9.             console.log('message:' + $scope.message);  
  10.           });  
  11.         }, 2000);  
  12.       }  
  13.         
  14.       $scope.getMessage();  
  15.       
  16.     });  

 

 

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

 

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

 

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

 

[javascript]  view plain  copy
 
 print?
  1. $scope.getMessage = function() {  
  2.   setTimeout(function() {  
  3.     $scope.message = 'Fetched after two seconds';  
  4.     console.log('message:' + $scope.message);  
  5.     $scope.$apply(); //this triggers a $digest  
  6.   }, 2000);  
  7. };  

 

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

相關文章
相關標籤/搜索