$watch How the $apply Runs a $digest

做者:junyuecao | 發表於 8-8 13:39 | 最後更新時間:8-9 02:34 javascript

原文地址:http://angular-tips.com/blog/2013/08/watch-how-the-apply-runs-a-digest/html

原譯文地址:http://junyuecao.ap01.aws.af.cm/article/junyuecao/52039f96def0e3be74000002java


這篇博文主要是寫給新手的,是給那些剛剛開始接觸Angular,而且想了解數據幫定是如何工做的人。若是你已經對Angular比較瞭解了,那強烈建議你直接去閱讀源代碼。瀏覽器

Angular用戶都想知道數據綁定是怎麼實現的。你可能會看到各類各樣的詞彙:$watch,$apply,$digest,dirty-checking...它們是什麼?它們是如何工做的呢?這裏我想回答這些問題,其實它們在官方的文檔裏都已經回答了,可是我仍是想把它們結合在一塊兒來說,可是我只是用一種簡單的方法來說解,若是要想了解技術細節,查看源代碼。ruby

讓咱們從頭開始吧。網絡

瀏覽器事件循環和Angular.js擴展

咱們的瀏覽器一直在等待事件,好比用戶交互。假如你點擊一個按鈕或者在輸入框裏輸入東西,事件的回調函數就會在javascript解釋器裏執行,而後你就能夠作任何DOM操做,等回調函數執行完畢時,瀏覽器就會相應地對DOM作出變化。 Angular拓展了這個事件循環,生成一個有時成爲angular context的執行環境(記住,這是個重要的概念),爲了解釋什麼是context以及它如何工做,咱們還須要解釋更多的概念。app


$watch 隊列($watch list)

每次你綁定一些東西到你的UI上時你就會往$watch隊列裏插入一條$watch。想象一下$watch就是那個能夠檢測它監視的model裏時候有變化的東西。例如你有以下的代碼函數

index.htmlpost

User: <input type="text" ng-model="user" />
Password: <input type="password" ng-model="pass" />

在這裏咱們有個$scope.user,他被綁定在了第一個輸入框上,還有個$scope.pass,它被綁定在了第二個輸入框上,而後咱們在$watch list裏面加入兩個$watch:spa

controllers.js

app.controller('MainCtrl', function($scope) {
  $scope.foo = "Foo";
  $scope.world = "World";
});

index.html

Hello, {{ World }}

這裏,即使咱們在$scope上添加了兩個東西,可是隻有一個綁定在了UI上,所以在這裏只生成了一個$watch. 再看下面的例子: controllers.js

app.controller('MainCtrl', function($scope) {
  $scope.people = [...];
});

index.html

<ul>
  <li ng-repeat="person in people">
      {{person.name}} - {{person.age}}
  </li>
</ul>

這裏又生成了多少個$watch呢?每一個person有兩個(一個name,一個age),而後ng-repeat又有一個,所以10個person一共是(2 * 10) +1,也就是說有21個$watch。 所以,每個綁定到了UI上的數據都會生成一個$watch。對,那這寫$watch是何時生成的呢? 當咱們的模版加載完畢時,也就是在linking階段(Angular分爲compile階段和linking階段---譯者注),Angular解釋器會尋找每一個directive,而後生成每一個須要的$watch。聽起來不錯哈,可是,而後呢?


$digest循環(這個digest不知道怎麼翻譯)

還記得我前面提到的擴展的事件循環嗎?當瀏覽器接收到能夠被angular context處理的事件時,$digest循環就會觸發。這個循環是由兩個更小的循環組合起來的。一個處理evalAsync隊列,另外一個處理$watch隊列,這個也是本篇博文的主題。 這個是處理什麼的呢?$digest將會遍歷咱們的$watch,而後詢問:

  • 嘿,$watch,你的值是什麼?
    • 是9。
  • 好的,它改變過嗎?
    • 沒有,先生。
  • (這個變量沒變過,那下一個)
  • 你呢,你的值是多少?
    • 報告,是Foo
  • 剛纔改變過沒?
    • 改變過,剛纔是Bar
  • (很好,咱們有DOM須要更新了)
  • 繼續詢問知道$watch隊列都檢查過。

這就是所謂的dirty-checking。既然全部的$watch都檢查完了,那就要問了:有沒有$watch更新過?若是有至少一個更新過,這個循環就會再次觸發,直到全部的$watch都沒有變化。這樣就可以保證每一個model都已經不會再變化。記住若是循環超過10次的話,它將會拋出一個異常,防止無限循環。 當$digest循環結束時,DOM相應地變化。

例如: controllers.js

app.controller('MainCtrl', function() {
  $scope.name = "Foo";

  $scope.changeFoo = function() {
      $scope.name = "Bar";
  }
});

index.html

{{ name }}
<button ng-click="changeFoo()">Change the name</button>

這裏咱們有一個$watch由於ng-click不生成$watch(函數是不會變的)。

  • 咱們按下按鈕
  • 瀏覽器接收到一個事件,進入angular context(後面會解釋爲何)。
  • $digest循環開始執行,查詢每一個$watch是否變化。
  • 因爲監視$scope.name$watch報告了變化,它會強制再執行一次$digest循環。
  • 新的$digest循環沒有檢測到變化。
  • 瀏覽器拿回控制權,更新與$scope.name新值相應部分的DOM。

這裏很重要的(也是許多人的很蛋疼的地方)是每個進入angular context的事件都會執行一個$digest循環,也就是說每次咱們輸入一個字母循環都會檢查整個頁面的全部$watch


經過$apply來進入angular context

誰決定什麼事件進入angular context,而哪些又不進入呢?$apply

若是當事件觸發時,你調用$apply,它會進入angular context,若是沒有調用就不會進入。如今你可能會問:剛纔的例子裏我也沒有調用$apply啊,爲何?Angular爲了作了!所以你點擊帶有ng-click的元素時,時間就會被封裝到一個$apply調用。若是你有一個ng-model="foo"的輸入框,而後你敲一個f,事件就會這樣調用$apply("foo = 'f';")


Angular何時不會自動爲咱們$apply呢?

這是Angular新手共同的痛處。爲何個人jQuery不會更新我綁定的東西呢?由於jQuery沒有調用$apply,事件沒有進入angular context$digest循環永遠沒有執行。

咱們來看一個有趣的例子:

假設咱們有下面這個directive和controller

app.js

app.directive('clickable', function() {

return {
  restrict: "E",
  scope: {
    foo: '=',
    bar: '='
  },
  template: '<ul style="background-color: lightblue"><li>{{foo}}</li><li>{{bar}}</li></ul>',
  link: function(scope, element, attrs) {
    element.bind('click', function() {
      scope.foo++;
      scope.bar++;
    });
  }
}

});

app.controller('MainCtrl', function($scope) {
  $scope.foo = 0;
  $scope.bar = 0;
});

它將foobar從controller裏綁定到一個list裏面,每次點擊這個元素的時候,foobar都會自增1。

那咱們點擊元素的時候會發生什麼呢?咱們能看到更新嗎?答案是否認的。由於點擊事件是一個沒有封裝到$apply裏面的常見的事件,這意味着咱們會失去咱們的計數嗎?不會

真正的結果是:$scope確實改變了,可是沒有強制$digest循環,監視foobar$watch沒有執行。也就是說若是咱們本身執行一次$apply那麼這些$watch就會看見這些變化,而後根據須要更新DOM。


試試看吧:http://jsbin.com/opimat/2/


若是咱們點擊這個directive(藍色區域),咱們看不到任何變化,可是咱們點擊按鈕時,點擊數就更新了。如剛纔說的,在這個directive上點擊時咱們不會觸發$digest循環,可是當按鈕被點擊時,ng-click會調用$apply,而後就會執行$digest循環,因而全部的$watch都會被檢查,固然就包括咱們的foobar$watch了。

如今你在想那並非你想要的,你想要的是點擊藍色區域的時候就更新點擊數。很簡單,執行一下$apply就能夠了:

element.bind('click', function() {
  scope.foo++;
  scope.bar++;

  scope.$apply();
});

$apply是咱們的$scope(或者是direcvie裏的link函數中的scope)的一個函數,調用它會強制一次$digest循環(除非當前正在執行循環,這種狀況下會拋出一個異常,這是咱們不須要在那裏執行$apply的標誌)。


試試看:http://jsbin.com/opimat/3/edit


有用啦!可是有一種更好的使用$apply的方法:

element.bind('click', function() {
  scope.$apply(function() {
      scope.foo++;
      scope.bar++;
  });
})

有什麼不同的?差異就是在第一個版本中,咱們是在angular context的外面更新的數據,若是有發生錯誤,Angular永遠不知道。很明顯在這個像個小玩具的例子裏面不會出什麼大錯,可是想象一下咱們若是有個alert框顯示錯誤給用戶,而後咱們有個第三方的庫進行一個網絡調用而後失敗了,若是咱們不把它封裝進$apply裏面,Angular永遠不會知道失敗了,alert框就永遠不會彈出來了。

所以,若是你想使用一個jQuery插件,而且要執行$digest循環來更新你的DOM的話,要確保你調用了$apply

有時候我想多說一句的是有些人在不得不調用$apply時會「感受不妙」,由於他們會以爲他們作錯了什麼。其實不是這樣的,Angular不是什麼魔術師,他也不知道第三方庫想要更新綁定的數據。


使用$watch來監視你本身的東西

你已經知道了咱們設置的任何綁定都有一個它本身的$watch,當須要時更新DOM,可是咱們若是要自定義本身的watches呢?簡單

來看個例子:

app.js

app.controller('MainCtrl', function($scope) {
  $scope.name = "Angular";

  $scope.updated = -1;

  $scope.$watch('name', function() {
    $scope.updated++;
  });
});

index.html

<body ng-controller="MainCtrl">
  <input ng-model="name" />
  Name updated: {{updated}} times.
</body>

這就是咱們創造一個新的$watch的方法。第一個參數是一個字符串或者函數,在這裏是只是一個字符串,就是咱們要監視的變量的名字,在這裏,$scope.name(注意咱們只須要用name)。第二個參數是當$watch說我監視的表達式發生變化後要執行的。咱們要知道的第一件事就是當controller執行到這個$watch時,它會當即執行一次,所以咱們設置updated爲-1。


試試看:http://jsbin.com/ucaxan/1/edit


例子2:

app.js

app.controller('MainCtrl', function($scope) {
  $scope.name = "Angular";

  $scope.updated = 0;

  $scope.$watch('name', function(newValue, oldValue) {
    if (newValue === oldValue) { return; } // AKA first run
    $scope.updated++;
  });
});

index.html

<body ng-controller="MainCtrl">
  <input ng-model="name" />
  Name updated: {{updated}} times.
</body>

watch的第二個參數接受兩個參數,新值和舊值。咱們能夠用他們來略過第一次的執行。一般你不須要略過第一次執行,但在這個例子裏面你是須要的。靈活點嘛少年。

例子3:

app.js

app.controller('MainCtrl', function($scope) {
  $scope.user = { name: "Fox" };

  $scope.updated = 0;

  $scope.$watch('user', function(newValue, oldValue) {
    if (newValue === oldValue) { return; }
    $scope.updated++;
  });
});

index.html

<body ng-controller="MainCtrl">
  <input ng-model="user.name" />
  Name updated: {{updated}} times.
</body>

咱們想要監視$scope.user對象裏的任何變化,和之前同樣這裏只是用一個對象來代替前面的字符串。


試試看:http://jsbin.com/ucaxan/3/edit


呃?沒用,爲啥?由於$watch默認是比較兩個對象所引用的是否相同,在例子1和2裏面,每次更改$scope.name都會建立一個新的基本變量,所以$watch會執行,由於對這個變量的引用已經改變了。在上面的例子裏,咱們在監視$scope.user,當咱們改變$scope.user.name時,對$scope.user的引用是不會改變的,咱們只是每次建立了一個新的$scope.user.name,可是$scope.user永遠是同樣的。

例子4:

app.js

app.controller('MainCtrl', function($scope) {
  $scope.user = { name: "Fox" };

  $scope.updated = 0;

  $scope.$watch('user', function(newValue, oldValue) {
    if (newValue === oldValue) { return; }
    $scope.updated++;
  }, true);
});

index.html

<body ng-controller="MainCtrl">
  <input ng-model="user.name" />
  Name updated: {{updated}} times.
</body>

試試看:http://jsbin.com/ucaxan/4/edit


如今有用了吧!由於咱們對$watch加入了第三個參數,它是一個bool類型的參數,表示的是咱們比較的是對象的值而不是引用。因爲當咱們更新$scope.user.name$scope.user也會改變,因此可以正確觸發。

關於$watch還有不少tips&tricks,可是這些都是基礎。

總結

好吧,我但願大家已經學會了在Angular中數據綁定是如何工做的。我猜測你的第一印象是dirty-checking很慢,好吧,實際上是不對的。它像閃電般快。可是,是的,若是你在一個模版裏有2000-3000個watch,它會開始變慢。可是我以爲若是你達到這個數量級,就能夠找個用戶體驗專家諮詢一下了

不管如何,隨着ECMAScript6的到來,在Angular將來的版本里咱們將會有Object.observe那樣會極大改善$digest循環的速度。同時將來的文章也會涉及一些tips&tricks。

另外一方面,這個主題並不容易,若是你發現我落下了什麼重要的東西或者有什麼東西徹底錯了,請指正(原文是在GITHUB上PR 或報告issue)

posted on 2013-08-19 16:58 司徒正美 閱讀(...) 評論(...) 編輯 收藏

相關文章
相關標籤/搜索