談談Angular關於$watch,$apply 以及 $digest的工作原理


這篇文章主要是面向那些剛開始學AngularJs和想要了解數據綁定(data-binding)是怎么工作的,

如果你已經熟悉如何使用angularjs了,我強烈建議你不用閱讀了。

 

angularjs使用者想要知道data-binding是如何工作的,就會遇到很多的關的術語

比如$wacth,$apply,$digest,dirty-checking(臟值檢測)...等等,這些又是做什么的呢?

在這篇文章里我會解決所有的疑問,通過結合這些術語在一起來學習。

但是我會盡量用簡單的方式來說明。

 

現在開始

The browser events-loop and the Angular.js extension

我們的瀏覽器會檢測等待事件發生,比如用戶的一些行為,假如你點擊了一個button或者在input寫東西,

事件的回調就會在內置的JavaScript跑起來,然后你就能夠做一些DOM操作了。

所以當回調發生的時候,瀏覽器中的DOM會發生一些變化。

 

而Angularjs擴展了這個事件輪詢,創建了一個叫angular content的東西(記住它,非常重要的一個概念),

為了解釋這個context是什么以及它是怎么工作的,我們需要先了解一下其他的一些概念。

The $watch list

每當你在ui上綁定了東西,就會添加了一個$wacth到$watch list中

你可以把$watch想象成為一個能夠察覺model的變化的檢測器,

比如你的html代碼是

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

在這里,我們將$scope.user綁定到了第一個input上,把$scope.pass綁定到了第二個input上,

這樣就意味着,我們添加了兩個$wacth到了$watch list中了。

 

再比如

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

這個例子中,雖然我們在控制器中定義了foo和world,但是只有一個綁定到了頁面上,

所以這個例子中,我們只創建了一個$watch。

 

我們再來看看下面這種情況:

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

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

那這個例子有多少個$watch被創建呢?

我們在這里假設peple數組中的每個元素都擁有name和age這2個屬性。

由於我們實用ng-repeat遍歷$scope.people,

如果有10個人,則我們一共創建了10*2+1 = 22

注意:其中的1,為ng-repeat所創建的。

 

所以要注意的是,當我們在ui上使用(綁定)用directives 指令的時候,就創建了一個$watch,

對了,那它是什么時候創建的呢?

當我們的模板加載完成(亦叫做linking phase),compiler就會找到所有的指令,並創建對應的$watch。

$digest loop

還記得我們在上面討論的event-loop嗎?

當瀏覽器發送一個事件,我們就能通過angular context管理這個事件,此時$digest就會被激活

這個事件輪詢loop由兩個小loop組成,

一個是處理$evalAsync隊列

一個是處理$watch list,這就是本文的主題了。

那處理的過程是怎樣的?$digest會輪詢我們有的$watch list,大概就想下面這樣

---》Hey,$watch,你的value值是多少啊?

---》我的value值是9。

---》好的,有變化嗎?

---》沒有。

(什么都沒有變化,就會跳到下個繼續詢問)

---》來,你的value值是多少啊?

---》是foo。

---》有變化嗎?

---》有啊,本來是bar的

(很好,那么現在我們有一個DOM要更新下了)

流程就會這樣繼續下去,知道$watch list中所有的$watch都被詢問...

 

現在講下臟值檢測(dirty-checking),現在所有的$watch都被輪詢過了,

會再次詢問是否所有的$watch都已經更新了?

如果其中有一個改變了,就會重新輪詢,直到所有的$wacth沒有改變。這樣做事為了確保所有的model都是“干凈的”

注意:如果輪詢loop超過十次,就會拋出異常,來退出無限的輪詢。

 

當$degest loop完成,DOM就會發生改變

看下面的例子

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

  $scope.changeFoo = function() {
      $scope.name = "Bar";
  }
});
{{ name }}
<button ng-click="changeFoo()">Change the name</button>

這里我們只有一個$watch,,因為ng-click不會創建任何的$watch(函數function是不會發生改變的)

1·當我們點擊按鈕的時候

2·瀏覽器發送事件,進入angular context(后面再解釋這個context)

3·然后執行$degest loop會輪詢所有的$watch是否發生改變

4·新的一輪loop報告說沒有新的改變了

5·然后瀏覽器就會拿回控制權,然后更新DOM,這里是更新新的值,$scope.name

這里最重要的一點(也是難點)是所有的事件會進入angular context然后執行$digest loop

這意味着每當我們在input鍵入一個字符,輪詢loop會檢查頁面上所有的$watch。

Entering the angular context with $apply

那是怎么進入angular context的呢?答案是$apply

當一個事件發生時嗎,你如果調用$apply,就會進入angular context。

如果你沒有調用,就只會在外部(這里指的是區別於angular context內部)執行

那么你就可能會有疑問了,

上面的那個例子我沒有調用$apply,它為什么會進入angular context呢?答案是Angular幫我們做了。

所有當你調用ng-click時候,這個事件就會被包含進了$apply調用了。

如果你在一個頁面上有input,並且標簽上寫着ng-model="foo",

然后輸入“f”,事件就像這樣$apply("foo = 'f';")被調用,換句話說,就是被$apply包含着調用了。

When angular doesn’t use $apply for us

這是很多Angular的新手共同的疑問,我在頁面上上使用了jQuery,為什么JQuery沒有更新我的綁定呢?

因為jQuery沒有調用$apply,所有的事件並沒有進入angular context中,所以$digest loop沒有執行

 

下面讓我們看看一個有趣的例子:

假如我們的代碼中有下面的指令directive 和控制器controller

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;
});

例子中我們綁定了foo和bar到了ul上的li中,我們想每當我們點擊的時候,foo和bar都會增加一

那實際情況中我們點擊之后會發生什么呢?我們能不能看到foo和bar按照我們預期的變化呢?

答案是不會,因為上面的click是沒有被包含在$apply中調用的。

這樣是否意味着我們不能這樣控制我們想要的變化呢?實際上不是的,我們是可以的

實際上$scope上的foo和bar都是變化了的,只是它沒有執行$digest loop,所以也就沒有意識到$watch的變化

這樣也意味着,如果我在之后調用$apply,這樣所有的$watch都會知道變化,然后就會更新相應的DOM

<!DOCTYPE html>
<html ng-app="app">
<head>
<script src="http:////cdn.staticfile.org/angular.js/1.2.10/angular.min.js"></script>
<meta charset=utf-8 />
<title>Directive example</title>
</head>
<body ng-controller="MainCtrl">
  <clickable foo="foo" bar="bar"></clickable>
  <hr />
  
  {{ hello }} <button ng-click="setHello()">Change hello</button>
</body>
</html>
app = angular.module('app', []);

app.controller('MainCtrl', function($scope) {
  $scope.foo = 0;
  $scope.bar = 0;
  
  $scope.hello = "Hello";
  
  $scope.setHello = function() {
    $scope.hello = "World";
  };
});

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++;
      });
    }
  }

});

如果你點擊上面藍色區域,你是不會看到任何變化的,但是之后再點擊button的話,

你就會突然看到0的數字變成一個數字,這個數字就是剛剛你點擊了藍色區域多少下的數字

就像剛剛我上面說的那樣,指令里的click並沒有觸發$digest loop,但是當你點擊button按鈕的時候

按鈕button上的ng-click就會調用$apply,然后就會執行$digest loop,

所有的$watch就會檢查有沒有改變(其中包含了foo和bar這兩個$watch)

 

這時,你可能會想,這樣的結果不是你想要的,你想要你點擊那個指令塊的時候馬上就能看到變化。

其實這很簡單,只需要調用的時候包含$apply就可以了

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

  scope.$apply();
});

$apply是$scope(或者是指令link function上的scope)上的一個函數function,所以調用它會強制執行$digest loop

注意,如果已經有一個輪詢loop了,在這種情況下調用它將拋出一個異常,這是一個跡象表明,我們不需要再調用$apply了。

其實上面的用法更好的是這樣使用

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

這兩種用法有什么區別呢?

區別是第一種用法中,我們更新新的值是在angular context外部,所以當有錯誤的時候,Angular是不會知道的。

顯然在上面的小例子中它不會產生多大影響,但是想像下當我們在復雜項目使用多種庫,然后出錯的時候,Angular是不會知道自己的錯誤

 

所以如果你想在在項目中使用 jQuery plugin,你要確保你有調用$apply來執行$degest loop來更新DOM的變化。

Conclusion

最后,我希望你看完就明白了 Angular中data-binding的工作原理,我猜你看完的第一印象會覺得dirty-checking是很慢的

實際上它是很快的,實際上只有頁面上達到2000-3000 個$watch的時候,它才會出現性能上的延遲,但是我覺得你應該用不到那么多個。

 

注:本文基本取自於$watch How the $apply Runs a $digest

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM