這篇文章主要是面向那些剛開始學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/