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

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

探索 $apply() $digest()

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

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

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

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

目前爲止還不錯!但是,有一個小問題。在上面的例子中, 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 來做出正確的響應。

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

讓我們來看一個例子。加入你有一個頁面,一旦該頁面加載完畢了,你希望在兩秒鐘之後顯示一條信息。你的實現可能是下面這個樣子的:

HTML:

<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()

原文地址

http://www.sitepoint.com/understanding-angulars-apply-digest/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章