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

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

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

 

angularjs使用者想要知道data-binding是如何工做的,就會遇到不少的關的術語java

好比$wacth,$apply,$digest,dirty-checking(髒值檢測)...等等,這些又是作什麼的呢?angularjs

在這篇文章裏我會解決全部的疑問,經過結合這些術語在一塊兒來學習。數組

可是我會盡可能用簡單的方式來講明。瀏覽器

 

如今開始app

The browser events-loop and the Angular.js extension

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

事件的回調就會在內置的JavaScript跑起來,而後你就可以作一些DOM操做了。oop

因此當回調發生的時候,瀏覽器中的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/ 

相關文章
相關標籤/搜索