AngularJS雙向數(shù)據(jù)綁定原理之$watch、$apply和$digest的應(yīng)用
引子
這篇文章是寫給AngularJS新手的,如果你已經(jīng)對AngularJS的雙向數(shù)據(jù)綁定有了深入的了解,直接去閱讀源代碼好了。
背景
AngularJS開發(fā)者都想知道雙向數(shù)據(jù)綁定是怎么實現(xiàn)的。與data-binding相關(guān)的術(shù)語琳瑯滿目: $watch,$apply,$digest,dirty-checking等等它們是如何工作的呢?讓我們從頭開始講起吧
AngularJS 的雙向數(shù)據(jù)綁定是被瀏覽器逼的
瀏覽器看上去很美,其實在數(shù)據(jù)交互這塊兒,由于瀏覽器的“不作為”,導(dǎo)致瀏覽器的數(shù)據(jù)刷新成為一個難題。具體來說,瀏覽器可以很容易地監(jiān)聽一個事件,比如:用戶點擊一個按鈕,或者在輸入框里輸入東西,為此還提供了事件回調(diào)函數(shù)的API,事件的回調(diào)函數(shù)就會在javascript解釋器里執(zhí)行;但反過來就沒這么簡單了,如果來自后臺的數(shù)據(jù)發(fā)生了變化,需要通知給瀏覽器,讓瀏覽器刷新,瀏覽器并沒有提供這樣的數(shù)據(jù)交互機制,對于開發(fā)者來說,這是一個難以逾越的障礙,怎么辦呢? AngularJS出現(xiàn)了,它通過$scope 很好地實現(xiàn)了雙向數(shù)據(jù)綁定,其背后的原理就是$watch,$apply,$digest,dirty-checking
$watch 隊列($watch list)
從字面上看,watch 是觀察的意思。 每次綁定一些東西到瀏覽器上時,就會往$watch隊列里插入一條$watch。想象一下$watch就是那個可以檢測它監(jiān)視的model里時候有變化的東西。例如你有如下的代碼
User: <input type="text" ng-model="user" /> Password: <input type="password" ng-model="pass" />
這里有個$scope.user,它被綁定在了第一個輸入框上,還有個$scope.pass,它被綁定在了第二個輸入框上;然后在$watch list里面加入兩個$watch:
創(chuàng)建一個 controllers.js 文件,代碼如下:
app.controller('MainCtrl', function($scope) { $scope.foo = "Foo"; $scope.world = "World"; });
對應(yīng)的html 文件, index.html 代碼如下:
Hello, {{ World }}
這里,即便在$scope上添加了兩個東西,但是只有一個綁定在了UI上,因此只生成了一個$watch. 再看下面的例子:
controllers.js
app.controller('MainCtrl', function($scope) { $scope.people = [...]; });
對應(yīng)的html文件 index.html
<ul> <li ng-repeat="person in people"> {{person.name}} - {{person.age}} </li> </ul>
這樣看來,又生成了多個$watch。每個person有兩個(一個name,一個age),然后ng-repeat是一個循環(huán),因此10個person一共是(2 * 10) +1,也就是說有21個$watch。 因此,每一個綁定到了瀏覽器上的數(shù)據(jù)都會生成一個$watch。對,那這寫$watch是什么時候生成的呢? 先回顧下AngularJS的加載原理
AngularJS的加載原理:
AngularJS的模板加載分為編譯(compile)和鏈接(linking)兩個階段,在linking階段,AngularJS解釋器會尋找每個directive,然后生成每個需要的$watch。對了,$watch就是在這個階段生成的。
接下來,開始用到 $digest了
$digest 循環(huán)
從字面上看,digest是 “消化”的意思,總感覺這個名字怪怪的,跟不可思議的是 dirty-checking, 字面意思“臟檢查”,還是不翻譯為好。原作者的本意肯定不是這個意思,只可意會不可言傳!
$digest 是一個循環(huán),它在循環(huán)做什么呢? $digest 在遍歷我們的$watch。 $digest 一個個地詢問$watch —— “嗨,你觀察的數(shù)據(jù)發(fā)生變化了沒?”
這個遍歷就是所謂的dirty-checking。既然所有的$watch都檢查完了,那就要問了:有沒有$watch更新過?如果有至少一個更新過,這個循環(huán)就會再次觸發(fā),直到所有的$watch都沒有變化。這樣就能夠保證每個model都已經(jīng)不會再變化。記住如果循環(huán)超過10次的話,它將會拋出一個異常,以免出現(xiàn)無限循環(huán)。 當$digest循環(huán)結(jié)束時,DOM相應(yīng)地變化。
看段代碼,例如: controllers.js
app.controller('MainCtrl', function() { $scope.name = "Foo"; $scope.changeFoo = function() { $scope.name = "Bar"; } });
對應(yīng)的html文件,index.html
{{ name }} <button ng-click="changeFoo()">Change the name</button>
這里只有一個$watch,因為ng-click不生成$watch(函數(shù)是不會變的)。
$digest 執(zhí)行的流程是:
- 在瀏覽器按下按鈕;
- 瀏覽器接收到一個事件,進入angular context。
- $digest循環(huán)開始執(zhí)行,查詢每個$watch是否變化。
- 由于監(jiān)視$scope.name的$watch報告了變化,它會強制再執(zhí)行一次$digest循環(huán)。
- 新的$digest循環(huán)沒有檢測到變化,此時瀏覽器拿回控制權(quán),更新與$scope.name新值相應(yīng)部分的DOM。
從中可以看出AngularJS的一個明顯的不足:每一個進入angular context的事件都會執(zhí)行一個$digest循環(huán),哪怕僅僅是輸入一個字母,$digest 都會遍歷整個頁面的所有$watch。
$apply 的應(yīng)用
Angular context 是整個Angular的上下文,也可以把它理解為Angular容器,那么,是誰來決定哪些事件可以進入 Angular Context,哪些事件又不能進入呢? 其控制器在 $apply手上。
如果當事件觸發(fā)時,調(diào)用$apply,它會進入angular context,如果沒有調(diào)用就不會進入。你可能會問:剛才的例子并沒有調(diào)用$apply,這是怎么回事呢?原來,是Angular背后替你做了。當點擊帶有ng-click的元素時,事件就會被封裝到一個$apply調(diào)用中。如果有一個ng-model="foo"的輸入框,當輸入一個字母 f 時,事件就會這樣調(diào)用,$apply("foo = 'f';")。
$apply的應(yīng)用場景
$apply是$scope的一個函數(shù),調(diào)用它會強制一次$digest循環(huán)。如果當前正在執(zhí)行$apply循環(huán),則會拋出一個異常。
如果瀏覽器上數(shù)據(jù)沒有及時刷新,可以通過調(diào)用$scope.$apply() 方法,強行刷新一遍。
通過 $watch 監(jiān)控自己的$scope
<!DOCTYPE html> <html ng-app="demoApp"> <head> <title>test</title> <!-- Vendor libraries --> <script src="lib/jquery-v1.11.1.js"></script> <script src="lib/angular-v1.2.22.js"></script> <script src="lib/angular-route-v1.2.22.js"></script> </head> <body> <div ng-controller="MainCtrl" > <input ng-model="name" /> Name updated: {{updated}} times. </div> <script > var demoApp = angular.module('demoApp',[]); demoApp.controller('MainCtrl', function($scope) { $scope.name = "Angular"; $scope.updated = -1; $scope.$watch('name', function() { $scope.updated++; }); }); </script> </body> </html>
代碼說明:
當controller 執(zhí)行到 $watch時,它會立即調(diào)用一次,所以把updated的值設(shè)為 -1 。 上輸入框中輸入字符發(fā)生變化時,你會看到 updated 的值隨之變化,而且能顯示變化的次數(shù)。
$watch 檢測到的數(shù)據(jù)變化
小結(jié)
我們對 AngularJS的雙向數(shù)據(jù)綁定有了一個初步的認識,對于AngularJS來說,表面上看操作DOM很簡單,其實背后有 $watch、$digest 、 $apply 三者在默默地起著作用。這個遍歷檢查數(shù)據(jù)是否發(fā)生變化的過程,稱之為:dirty-checking。 當你了解了這個過程后,你會對它嗤之以鼻,感覺這種方法好low 哦。 確實,如果一個DOM中有 2000- 3000個 watch,頁面的渲染速度將會大打折扣。
這個渲染的性能問題怎么解決呢?隨著ECMAScript6的到來,Angular 2 通過Object.observe 極大地改善$digest循環(huán)的速度。或許,這就是為什么 Angular 團隊迫不及待地推出 Angular 2 的原因吧。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
AngularJS基礎(chǔ) ng-open 指令簡單實例
本文主要介紹AngularJS ng-open 指令,這里幫大家整理了ng-open指令的基本資料,有需要的小伙伴可以參考下2016-08-08使用AngularJS來實現(xiàn)HTML頁面嵌套的方法
這篇文章主要介紹了使用AngularJS來實現(xiàn)HTML頁面嵌套的方法,主要用到了AngularJS中的ng-include指令,需要的朋友可以參考下2015-06-06微信小程序?qū)崿F(xiàn)左右聯(lián)動的實戰(zhàn)記錄
聯(lián)動菜單是大家在開發(fā)小程序經(jīng)常會遇到的一個功能,下面這篇文章主要給大家介紹了關(guān)于微信小程序?qū)崿F(xiàn)左右聯(lián)動的相關(guān)資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07AngularJS實現(xiàn)的select二級聯(lián)動下拉菜單功能示例
這篇文章主要介紹了AngularJS實現(xiàn)的select二級聯(lián)動下拉菜單功能,結(jié)合完整實例形式分析了AngularJS實現(xiàn)二級聯(lián)動菜單的具體操作步驟與相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2017-10-10