理解 Angular 中的 $digest() 和 $apply()

翻譯自 http://www.sitepoint.com/understanding-angulars-apply-digestjavascript

$digest()$apply()是AngularJS中的兩個核心而且有時候容易引人誤解的部分。咱們須要深刻理解這二者是如何運做的,從而才能理解AngularJS自己是如何運做的。本文的目的就是爲了和你解釋,在你的日復一日使用AngularJS編寫代碼的過程當中,$digest()$apply()是如何確確實實的對你有用的。html

$digest()$apply()的探索

AngularJS爲咱們提供了一個廣爲人知的使人驚歎的功能,那就是數據的雙向綁定。這個功能極大的方便了咱們的編碼。數據綁定意味着,當視圖(view)中的數據發生變化的時候,做用域下的數據模型(model)也會相應的更新。一樣的,不管什麼時候,當數據模型(model)發生變化的時候,視圖(view)也會相應的更新。那麼,AngularJS是怎麼作到的呢?當你寫這樣的一個表達式的時候( {{ aModel }} ),AngularJS在背後爲這個數據模型(model)設置了一個監聽器(watcher),正是因爲這個監聽器(watcher),不管什麼時候當數據模型(model)發生變化的時候,視圖(view)也會更新。這個監聽器(watcher)與你在AngularJS中設置的其它監聽器相似(watcher):java

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

你們都知道,傳入$watch()的第二個參數是一個監聽函數,而且,不管什麼時候當aModel的值發生改變的時候,他都會被調用。對於咱們來講,這樣理解很簡單,當aModel發生改變的時候,這個監聽函數被調用,而後,更新HTML中的表達式的值。可是,有一個很大的問題在於,AngularJS是怎麼知道何時去調用這個監聽函數呢?換句話說,AngularJS是怎麼知道aModel發生變化的從而去調用相對應的監聽函數呢?難道是週期性的運行一個函數來檢測數據模型是否發生了變化嗎?好吧,這時候,就須要講到$digest循環了。app

監聽器(watcher)是在$digest循環中被啓用的。當一個監聽器(watcher)被啓用的時候,AngularJS就會去評估數據模型(model),而且,當數據模型(model)的值發生改變的時候,就會去調用相應的監聽函數。因此,下一個問題就是,$digest循環是怎麼開始的。函數

當運行$scope.$digest()的時候,$digest循環就開始了。假設你經過ng-click指令來調用處理函數來改變一個數據模型(model),在這種狀況下,AngularJS就會自動調用$digest()觸發$digest循環。當$digest循環開始的時候,他就會啓動每個監聽器(watcher)。這些監聽器(watcher)會去檢查當前的數據模型(model)中的值是否與最後一次計算的值相同,若是不相同,那麼,對應的監聽函數就會被執行。結果就是,若是你的視圖(view)中有表達式,那麼,這些表達式就都會被更新。除了ng-click,還有一些其它的內置指令(或者服務)可讓你改變數據模型(好比ng-model$timeout等),而且自動觸發$digest循環。this

到目前爲止,很棒有沒有?可是,仍然有一些小的不足。在上面的例子中,AngularJS並非直接調用$digest(),而是經過$scope.$apply()而後,相應的調用$rootScope.$digest()。這樣作的結果就是,一個$digest循環在$rootScope開始,隨後,逐步的遍歷全部的子做用域來調用監聽器(watcher)。編碼

如今,假設你給一個按鈕添加了ng-click指令,而且爲他傳遞了一個函數。當你點擊這個按鈕的時候,AngularJS會把這個函數放進$scope.$apply()中進行調用。因此,你的函數可以正常的執行,改變數據模型(若是有的話),而且,$digest循環會開始來保證你的更改會反映到視圖(view)中。翻譯

注意:$scope.$apply()會自動調用$rootScope.$digest()$apply()函數能夠以兩種方式運行。第一種是將函數做爲參數,而且評估他,而後觸發$digest循環。第二種並不傳入任何參數,僅僅是執行一個$digest循環。咱們將會知道爲何前一種方法是更好更直接的方法。雙向綁定

何時須要人爲的調用$apply()

若是AngularJS老是將咱們的代碼放在$apply()中而且執行$digest循環,那麼,何時須要咱們人爲的調用$apply呢?事實上,AngularJS已經很明確的告訴咱們了。AngularJS只會關心在AngularJS的執行上下文中的發生的數據模型(model)的變化(好比,改變數據的代碼在$apply()裏面)。AngularJS內建的指令也會自動觸發$digest循環因此任何數據模型(model)的改變都會反映到視圖中。可是,若是咱們更改一個不在AngularJS執行上下文中的數據模型(model),就須要人爲的調用$apply()來提醒AngularJS數據發生變化了。就像是要告訴AngularJS,咱們改變了一些數據,他應該啓用監聽器以便於讓咱們所作的改變可以反映出來。code

例如,當使用Javascript的setTimeout()函數來更新一個數據模型的時候,AngularJS就沒辦法知道你改變了數據模型。這種狀況下,調用$apply()來觸發$digest循環就是你的責任了。相似的,若是你寫了一個指令,這個指令是設置了一個DOM事件監聽器,更改數據模型的代碼在事件處理函數裏,那麼,也須要調用$apply()來保證更改可以反映出來。

讓咱們來看個例子。假設有一個頁面,咱們想在頁面加載完以後兩秒顯示一個信息。咱們可能這樣寫:

<body ng-app="myApp">
    <div ng-controller="MessageController">
        Delayed Message: {{message}}
    </div>  
</body>
/* 沒有$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();
});

經過運行這個例子,咱們會發現,在兩秒以後,函數開始執行,而且更新數據模型。可是,視圖卻沒有更新。緣由咱們也應該猜到了,咱們忘了調用$apply()。所以,咱們須要像下面這樣來寫咱們的getMessage()函數。

/* 使用了$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();
});

運行更新後的例子,兩秒以後,咱們就可以看到視圖更新了。咱們所作的惟一的改變就是將代碼放進了$scope.$apply()中。這樣,就能自動的去調用$rootScope.$digest(),而後,監聽器就會啓動,視圖就會更新了。

注意:順便說一句,當須要延時的時候,儘量的使用$timout,這樣,就不用人爲的去調用$appply()了。

而且,注意看上面的代碼,咱們也能夠在調用$apply()的時候不傳參數。就像下面這樣:

$scope.getMessage = function() {
    setTimeout(function() {
        $scope.message = 'Fetched after two seconds';
        console.log('message:' + $scope.message);
        $scope.$apply(); // 這兒就觸發了$digest循環
    }, 2000);
};

上面的代碼調用$apply()的時候沒有傳入參數,可是也起做用了。記住,在調用$apply()的時候,應該老是要傳入函數參數。由於,當你爲$apply()傳入函數的時候,這個函數在調用的時候是包含在try..catch中的,而且,任何發生的異常都可以被$exceptionHandler服務所接收。

$digest循環要執行多少次呢

當一個$digest循環運行的時候,監聽器就會被執行以查看數據模型是否發生改變。若是改變了,對應的監聽函數就會被執行。這就致使了一個很重要的問題,假如一個監聽函數本身改變了數據模型,AngularJS怎麼知道呢?

答案就是,$digest循環並不僅是運行一次。在當前循環的結束以後,他會再次啓動來檢查是否有數據發生改變。這被叫作髒檢查,用來對那些可能被監聽函數所更改的數據模型進行監測。因此,$digest循環會一直保持循環直到再也沒有數據模型發生改變,或者,到達最大的循環次數(10次)。保持冪等以及最小化監聽函數裏的數據模型的更改老是比較好的。

注意:$digest至少會循環兩次即便監聽函數沒有更改任何數據模型。正如上文討論的那樣,他會多運行一次以確保沒有數據發生改變。

總結

我但願這篇文章可以清楚的描述$digest$apply是什麼。記住最重要的就是AngularJS是否可以監測到你的更改。若是不能,那麼,你就得必須人爲調用$apply()

相關文章
相關標籤/搜索