### 參考:
http://www.zhihu.com/question/23275373
http://www.angularjs.cn/A0a6
https://github.com/xufei/blog/issues/10
https://github.com/atian25/blog/issues/5
http://www.mamicode.com/info-detail-287407.html
# 理解$watch ,$apply 和 $digest --- 理解數(shù)據(jù)綁定過(guò)程
[TOC]
> 原文地址:http://angular-tips.com/blog/2013/08/watch-how-the-apply-runs-a-digest/
## 瀏覽器事件循環(huán)和Angular.js擴(kuò)展
我們的瀏覽器一直在等待事件,比如用戶(hù)交互。假如你點(diǎn)擊一個(gè)按鈕或者在輸入框里輸入東西,事件的回調(diào)函數(shù)就會(huì)在javascript解釋器里執(zhí)行,然后你就可以做任何DOM操作,等回調(diào)函數(shù)執(zhí)行完畢時(shí),瀏覽器就會(huì)相應(yīng)地對(duì)DOM做出變化。 Angular拓展了這個(gè)事件循環(huán),生成一個(gè)有時(shí)成為angular context的執(zhí)行環(huán)境(記住,這是個(gè)重要的概念),為了解釋什么是context以及它如何工作,我們還需要解釋更多的概念。
### $watch 隊(duì)列($watch list)
每次你綁定一些東西到你的UI上時(shí)你就會(huì)往$watch隊(duì)列里插入一條$watch。想象一下$watch就是那個(gè)可以檢測(cè)它監(jiān)視的model里時(shí)候有變化的東西。例如你有如下的代碼
index.html
~~~
User: <input type="text" ng-model="user" />
Password: <input type="password" ng-model="pass" />
~~~
在這里我們有個(gè)$scope.user,他被綁定在了第一個(gè)輸入框上,還有個(gè)$scope.pass,它被綁定在了第二個(gè)輸入框上,然后我們?cè)?watch list里面加入兩個(gè)$watch:
controllers.js
~~~
app.controller('MainCtrl', function($scope) {
$scope.foo = "Foo";
$scope.world = "World";
});
~~~
index.html:
`Hello, {{ World }}`
這里,即便我們?cè)?scope上添加了兩個(gè)東西,但是只有一個(gè)綁定在了UI上,因此在這里只生成了一個(gè)$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>
~~~
這里又生成了多少個(gè)$watch呢?每個(gè)person有兩個(gè)(一個(gè)name,一個(gè)age),然后ng-repeat又有一個(gè),因此10個(gè)person一共是(2 * 10) +1,也就是說(shuō)有21個(gè)$watch。 因此,每一個(gè)綁定到了UI上的數(shù)據(jù)都會(huì)生成一個(gè)$watch。對(duì),那這寫(xiě)$watch是什么時(shí)候生成的呢? 當(dāng)我們的模版加載完畢時(shí),也就是在linking階段(Angular分為compile階段和linking階段---譯者注),Angular解釋器會(huì)尋找每個(gè)directive,然后生成每個(gè)需要的$watch。聽(tīng)起來(lái)不錯(cuò)哈,但是,然后呢?
### $digest循環(huán)
還記得我前面提到的擴(kuò)展的事件循環(huán)嗎?當(dāng)瀏覽器接收到可以被angular context處理的事件時(shí),$digest循環(huán)就會(huì)觸發(fā)。這個(gè)循環(huán)是由兩個(gè)更小的循環(huán)組合起來(lái)的。一個(gè)處理evalAsync隊(duì)列,另一個(gè)處理$watch隊(duì)列,這個(gè)也是本篇博文的主題。 這個(gè)是處理什么的呢?$digest將會(huì)遍歷我們的$watch,然后詢(xún)問(wèn):
* 嘿,$watch,你的值是什么?
* 是9。
* 好的,它改變過(guò)嗎?
* 沒(méi)有,先生。
* (這個(gè)變量沒(méi)變過(guò),那下一個(gè))
* 你呢,你的值是多少?
* 報(bào)告,是Foo。
* 剛才改變過(guò)沒(méi)?
* 改變過(guò),剛才是Bar。
* (很好,我們有DOM需要更新了)
* 繼續(xù)詢(xún)問(wèn)直到$watch隊(duì)列都檢查過(guò)。
這就是所謂的dirty-checking。既然所有的$watch都檢查完了,那就要問(wèn)了:有沒(méi)有$watch更新過(guò)?如果有至少一個(gè)更新過(guò),這個(gè)循環(huán)就會(huì)再次觸發(fā),直到所有的$watch都沒(méi)有變化。這樣就能夠保證每個(gè)model都已經(jīng)不會(huì)再變化。記住如果循環(huán)超過(guò)10次的話(huà),它將會(huì)拋出一個(gè)異常,防止無(wú)限循環(huán)。 當(dāng)$digest循環(huán)結(jié)束時(shí),DOM相應(yīng)地變化。
例如: 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>
~~~
這里我們有一個(gè)$watch因?yàn)閚g-click不生成$watch(函數(shù)是不會(huì)變的)。
* 我們按下按鈕
* 瀏覽器接收到一個(gè)事件,進(jìn)入angular context(后面會(huì)解釋為什么)。
* $digest循環(huán)開(kāi)始執(zhí)行,查詢(xún)每個(gè)$watch是否變化。
* 由于監(jiān)視$scope.name的$watch報(bào)告了變化,它會(huì)強(qiáng)制再執(zhí)行一次$digest循環(huán)。
* 新的$digest循環(huán)沒(méi)有檢測(cè)到變化。
* 瀏覽器拿回控制權(quán),更新與$scope.name新值相應(yīng)部分的DOM。
這里很重要的(也是許多人的很蛋疼的地方)是每一個(gè)進(jìn)入angular context的事件都會(huì)執(zhí)行一個(gè)$digest循環(huán),也就是說(shuō)每次我們輸入一個(gè)字母循環(huán)都會(huì)檢查整個(gè)頁(yè)面的所有$watch。
### 通過(guò)$apply來(lái)進(jìn)入angular context
誰(shuí)決定什么事件進(jìn)入angular context,而哪些又不進(jìn)入呢?$apply!
如果當(dāng)事件觸發(fā)時(shí),你調(diào)用$apply,它會(huì)進(jìn)入angular context,如果沒(méi)有調(diào)用就不會(huì)進(jìn)入?,F(xiàn)在你可能會(huì)問(wèn):剛才的例子里我也沒(méi)有調(diào)用$apply啊,為什么?Angular為了做了!因此你點(diǎn)擊帶有ng-click的元素時(shí),時(shí)間就會(huì)被封裝到一個(gè)$apply調(diào)用。如果你有一個(gè)`ng-model="foo"`的輸入框,然后你敲一個(gè)f,事件就會(huì)這樣調(diào)用`$apply("foo = 'f';")`。
### $digest和$apply區(qū)別:
>https://github.com/xufei/blog/issues/10
* $apply可以帶參數(shù),它可以接受一個(gè)函數(shù),然后在應(yīng)用數(shù)據(jù)之后,調(diào)用這個(gè)函數(shù)。
~~~
scope.$digest(); //這個(gè)換成$apply即可
scope.$apply(function() {
scope.counter++;
});
~~~
* 對(duì)于$digest來(lái)說(shuō),在父作用域和子作用域上調(diào)用是有差別的
~~~
<div ng-app="test">
<div ng-controller="OuterCtrl">
<div ng-controller="InnerCtrl">
<button increaseb>increase b</button>
<span ng-bind="b"></span>
</div>
<button increasea>increase a</button>
<span ng-bind="a"></span>
</div>
</div>
~~~
上面的例子,我們就能看出差別了,在increase b按鈕上點(diǎn)擊,這時(shí)候,a跟b的值其實(shí)都已經(jīng)變化了,但是界面上的a沒(méi)有更新,直到點(diǎn)擊一次increase a,這時(shí)候剛才對(duì)a的累加才會(huì)一次更新上來(lái)。怎么解決這個(gè)問(wèn)題呢?只需在increaseb這個(gè)指令的實(shí)現(xiàn)中,把$digest換成$apply即可。
**當(dāng)調(diào)用$digest的時(shí)候,只觸發(fā)當(dāng)前作用域和它的子作用域上的監(jiān)控,但是當(dāng)調(diào)用$apply的時(shí)候,會(huì)觸發(fā)作用域樹(shù)上的所有監(jiān)控。**
因此,從性能上講,如果能確定自己作的這個(gè)數(shù)據(jù)變更所造成的影響范圍,應(yīng)當(dāng)盡量調(diào)用$digest,只有當(dāng)無(wú)法精確知道數(shù)據(jù)變更造成的影響范圍時(shí),才去用$apply,很暴力地遍歷整個(gè)作用域樹(shù),調(diào)用其中所有的監(jiān)控。
從另外一個(gè)角度,我們也可以看到,為什么調(diào)用外部框架的時(shí)候,是推薦放在$apply中,因?yàn)橹挥羞@個(gè)地方才是對(duì)所有數(shù)據(jù)變更都應(yīng)用的地方,如果用$digest,有可能臨時(shí)丟失數(shù)據(jù)變更。
### Angular什么時(shí)候不會(huì)自動(dòng)為我們$apply呢?
這是Angular新手共同的痛處。為什么我的jQuery不會(huì)更新我綁定的東西呢?因?yàn)閖Query沒(méi)有調(diào)用$apply,事件沒(méi)有進(jìn)入angular context,$digest循環(huán)永遠(yuǎn)沒(méi)有執(zhí)行。
我們來(lái)看一個(gè)有趣的例子:
假設(shè)我們有下面這個(gè)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;
});
~~~
它將foo和bar從controller里綁定到一個(gè)list里面,每次點(diǎn)擊這個(gè)元素的時(shí)候,foo和bar都會(huì)自增1。
那我們點(diǎn)擊元素的時(shí)候會(huì)發(fā)生什么呢?我們能看到更新嗎?答案是否定的。因?yàn)辄c(diǎn)擊事件是一個(gè)沒(méi)有封裝到$apply里面的常見(jiàn)的事件,這意味著我們會(huì)失去我們的計(jì)數(shù)嗎?不會(huì)
真正的結(jié)果是:$scope確實(shí)改變了,但是沒(méi)有強(qiáng)制$digest循環(huán),監(jiān)視foo 和bar的$watch沒(méi)有執(zhí)行。也就是說(shuō)如果我們自己執(zhí)行一次$apply那么這些$watch就會(huì)看見(jiàn)這些變化,然后根據(jù)需要更新DOM。
試試看吧:http://jsbin.com/opimat/2/
如果我們點(diǎn)擊這個(gè)directive(藍(lán)色區(qū)域),我們看不到任何變化,但是我們點(diǎn)擊按鈕時(shí),點(diǎn)擊數(shù)就更新了。如剛才說(shuō)的,在這個(gè)directive上點(diǎn)擊時(shí)我們不會(huì)觸發(fā)$digest循環(huán),但是當(dāng)按鈕被點(diǎn)擊時(shí),ng-click會(huì)調(diào)用$apply,然后就會(huì)執(zhí)行$digest循環(huán),于是所有的$watch都會(huì)被檢查,當(dāng)然就包括我們的foo和bar的$watch了。
現(xiàn)在你在想那并不是你想要的,你想要的是點(diǎn)擊藍(lán)色區(qū)域的時(shí)候就更新點(diǎn)擊數(shù)。很簡(jiǎn)單,執(zhí)行一下$apply就可以了:
~~~
element.bind('click', function() {
scope.foo++;
scope.bar++;
scope.$apply();
});
~~~
$apply是我們的$scope(或者是direcvie里的link函數(shù)中的scope)的一個(gè)函數(shù),調(diào)用它會(huì)強(qiáng)制一次$digest循環(huán)(除非當(dāng)前正在執(zhí)行循環(huán),這種情況下會(huì)拋出一個(gè)異常,這是我們不需要在那里執(zhí)行$apply的標(biāo)志)。
試試看:http://jsbin.com/opimat/3/edit
有用啦!但是有一種更好的使用$apply的方法:
~~~
element.bind('click', function() {
scope.$apply(function() {
scope.foo++;
scope.bar++;
});
})
~~~
有什么不一樣的?差別就是在第一個(gè)版本中,我們是在angular context的外面更新的數(shù)據(jù),如果有發(fā)生錯(cuò)誤,Angular永遠(yuǎn)不知道。很明顯在這個(gè)像個(gè)小玩具的例子里面不會(huì)出什么大錯(cuò),但是想象一下我們?nèi)绻袀€(gè)alert框顯示錯(cuò)誤給用戶(hù),然后我們有個(gè)第三方的庫(kù)進(jìn)行一個(gè)網(wǎng)絡(luò)調(diào)用然后失敗了,如果我們不把它封裝進(jìn)$apply里面,Angular永遠(yuǎn)不會(huì)知道失敗了,alert框就永遠(yuǎn)不會(huì)彈出來(lái)了。
因此,如果你想使用一個(gè)jQuery插件,并且要執(zhí)行$digest循環(huán)來(lái)更新你的DOM的話(huà),要確保你調(diào)用了$apply。
有時(shí)候我想多說(shuō)一句的是有些人在不得不調(diào)用$apply時(shí)會(huì)“感覺(jué)不妙”,因?yàn)樗麄儠?huì)覺(jué)得他們做錯(cuò)了什么。其實(shí)不是這樣的,Angular不是什么魔術(shù)師,他也不知道第三方庫(kù)想要更新綁定的數(shù)據(jù)。
### 使用$watch來(lái)監(jiān)視你自己的東西
你已經(jīng)知道了我們?cè)O(shè)置的任何綁定都有一個(gè)它自己的$watch,當(dāng)需要時(shí)更新DOM,但是我們?nèi)绻远x自己的watches呢?簡(jiǎn)單
來(lái)看個(gè)例子:
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>
~~~
這就是我們創(chuàng)造一個(gè)新的$watch的方法。第一個(gè)參數(shù)是一個(gè)字符串或者函數(shù),在這里是只是一個(gè)字符串,就是我們要監(jiān)視的變量的名字,在這里,$scope.name(注意我們只需要用name)。第二個(gè)參數(shù)是當(dāng)$watch說(shuō)我監(jiān)視的表達(dá)式發(fā)生變化后要執(zhí)行的。我們要知道的第一件事就是當(dāng)controller執(zhí)行到這個(gè)$watch時(shí),它會(huì)立即執(zhí)行一次,因此我們?cè)O(shè)置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的第二個(gè)參數(shù)接受兩個(gè)參數(shù),新值和舊值。我們可以用他們來(lái)略過(guò)第一次的執(zhí)行。通常你不需要略過(guò)第一次執(zhí)行,但在這個(gè)例子里面你是需要的。靈活點(diǎn)嘛少年。
例子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>
~~~
我們想要監(jiān)視$scope.user對(duì)象里的任何變化,和以前一樣這里只是用一個(gè)對(duì)象來(lái)代替前面的字符串。
試試看:http://jsbin.com/ucaxan/3/edit
呃?沒(méi)用,為啥?因?yàn)?watch默認(rèn)是比較兩個(gè)對(duì)象所引用的是否相同,在例子1和2里面,每次更改$scope.name都會(huì)創(chuàng)建一個(gè)新的基本變量,因此$watch會(huì)執(zhí)行,因?yàn)閷?duì)這個(gè)變量的引用已經(jīng)改變了。在上面的例子里,我們?cè)诒O(jiān)視$scope.user,當(dāng)我們改變$scope.user.name時(shí),對(duì)$scope.user的引用是不會(huì)改變的,我們只是每次創(chuàng)建了一個(gè)新的$scope.user.name,但是$scope.user永遠(yuǎn)是一樣的。
例子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
現(xiàn)在有用了吧!因?yàn)槲覀儗?duì)$watch加入了第三個(gè)參數(shù),它是一個(gè)bool類(lèi)型的參數(shù),表示的是我們比較的是對(duì)象的值而不是引用。由于當(dāng)我們更新$scope.user.name時(shí)$scope.user也會(huì)改變,所以能夠正確觸發(fā)。
關(guān)于$watch還有很多tips&tricks,但是這些都是基礎(chǔ)。
## 總結(jié)
好吧,我希望你們已經(jīng)學(xué)會(huì)了在Angular中數(shù)據(jù)綁定是如何工作的。我猜想你的第一印象是dirty-checking很慢,好吧,其實(shí)是不對(duì)的。它像閃電般快。但是,是的,如果你在一個(gè)模版里有2000-3000個(gè)watch,它會(huì)開(kāi)始變慢。但是我覺(jué)得如果你達(dá)到這個(gè)數(shù)量級(jí),就可以找個(gè)用戶(hù)體驗(yàn)專(zhuān)家咨詢(xún)一下了
無(wú)論如何,隨著ECMAScript6的到來(lái),在Angular未來(lái)的版本里我們將會(huì)有Object.observe那樣會(huì)極大改善$digest循環(huán)的速度。同時(shí)未來(lái)的文章也會(huì)涉及一些tips&tricks。
另一方面,這個(gè)主題并不容易,如果你發(fā)現(xiàn)我落下了什么重要的東西或者有什么東西完全錯(cuò)了,請(qǐng)指正(原文是在GITHUB上PR 或報(bào)告issue)
- 步入JavaScript的世界
- 二進(jìn)制運(yùn)算
- JavaScript 的版本是怎么回事?
- JavaScript和DOM的產(chǎn)生與發(fā)展
- DOM事件處理
- js的并行加載與順序執(zhí)行
- 正則表達(dá)式
- 當(dāng)遇上this時(shí)
- Javascript中apply、call、bind
- JavaScript的編譯過(guò)程與運(yùn)行機(jī)制
- 執(zhí)行上下文(Execution Context)
- javascript 作用域
- 分組中的函數(shù)表達(dá)式
- JS之constructor屬性
- Javascript 按位取反運(yùn)算符 (~)
- EvenLoop 事件循環(huán)
- 異步編程
- JavaScript的九個(gè)思維導(dǎo)圖
- JavaScript奇淫技巧
- JavaScript:shim和polyfill
- ===值得關(guān)注的庫(kù)===
- ==文章==
- JavaScript框架
- Angular 1.x
- 啟動(dòng)引導(dǎo)過(guò)程
- $scope作用域
- $q與promise
- ngRoute 和 ui-router
- 雙向數(shù)據(jù)綁定
- 規(guī)范和性能優(yōu)化
- 自定義指令
- Angular 事件
- lodash
- Test
