這篇文章主要是面向那些剛開始學AngularJs和想要了解數據綁定(data-binding)是怎麼工做的,javascript
若是你已經熟悉如何使用angularjs了,我強烈建議你不用閱讀了。html
angularjs使用者想要知道data-binding是如何工做的,就會遇到不少的關的術語java
好比$wacth,$apply,$digest,dirty-checking(髒值檢測)...等等,這些又是作什麼的呢?angularjs
在這篇文章裏我會解決全部的疑問,經過結合這些術語在一塊兒來學習。數組
可是我會盡可能用簡單的方式來講明。瀏覽器
如今開始app
咱們的瀏覽器會檢測等待事件發生,好比用戶的一些行爲,假如你點擊了一個button或者在input寫東西,函數
事件的回調就會在內置的JavaScript跑起來,而後你就可以作一些DOM操做了。oop
因此當回調發生的時候,瀏覽器中的DOM會發生一些變化。性能
而Angularjs擴展了這個事件輪詢,建立了一個叫angular content的東西(記住它,很是重要的一個概念),
爲了解釋這個context是什麼以及它是怎麼工做的,咱們須要先了解一下其餘的一些概念。
每當你在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。
還記得咱們在上面討論的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。
那是怎麼進入angular context的呢?答案是$apply
當一個事件發生時嗎,你若是調用$apply,就會進入angular context。
若是你沒有調用,就只會在外部(這裏指的是區別於angular context內部)執行
那麼你就可能會有疑問了,
上面的那個例子我沒有調用$apply,它爲何會進入angular context呢?答案是Angular幫咱們作了。
全部當你調用ng-click時候,這個事件就會被包含進了$apply調用了。
若是你在一個頁面上有input,而且標籤上寫着ng-model="foo",
而後輸入「f」,事件就像這樣$apply("foo = 'f';")被調用,換句話說,就是被$apply包含着調用了。
這是不少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的變化。
最後,我但願你看完就明白了 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/