watcher是Angular提供的一種對變量的觀察機制,如果存在數(shù)據(jù)的綁定等,則在不同的作用域中都存在相應的 watch list, 這些被觀察的變量將在 digest process (后面會詳講)中得到更新,這也稱之為 臟值檢查。
watch 和 watch listener
在 $scope (和$rootScope) 對象上存在 $watch 函數(shù),我們可以手動的添加watch listener, 當被觀察的變量被檢測到發(fā)生變化時,變量被更新,然后angularjs自動調(diào)用 watch listener.
// 觀察 'a' 變量的變化
// 后面的回調(diào)函數(shù)就是 watch listener, 2個參數(shù): newValue oldValue
$scope.$watch('a', function(newValue, oldValue) {
if (newValue != oldValue) {
$scope.b = $scope.a * 2
}
})
watches的類型
1.$watch - Reference watch 引用類型的觀測
$watch 是一種引用類型的觀察,只有當引用的地址發(fā)生變化時才觸發(fā),上面的代碼使用原始類型,當值變化時,相應的引用也會發(fā)生變化。下面例子中 obj 是一個對象,只有 obj 引用的地址發(fā)生變化時,才會調(diào)用watch listener, 因此當 a, b, c的值發(fā)生變化時,obj引用的地址并不會發(fā)生改變,因此不會調(diào)用watch listener,這一點要注意:
$scope.obj = {
a: 1,
b: 2,
c: 4
}
// 引用觀察
$scope.$watch('obj', function(newValue, oldValue) {
if ($newValue != oldValue) {
$scope.obj.c = $scope.obj.a * $scope.obj.b;
}
});
$watch with 'true' - Equality watch
同上面的例子,如果后面再添加一個 'true', 則觀察的類型將變?yōu)橄嗟刃杂^察(也可稱為deep watch)。這樣對象的成員發(fā)生變化時,會自動調(diào)用watch listener
$scope.obj = {
a: 1,
b: 2,
c: 4
}
// 相等性觀察
$scope.$watch('obj', function(newValue, oldValue) {
if ($newValue != oldValue) {
$scope.obj.c = $scope.obj.a * $scope.obj.b;
}
}, true);
2.$watchGroup - Reference watch for multiple variables
$watchGroup 對多個變量同時進行監(jiān)測,這可以說是一種簡便的寫法
$scope.a =1;
$scope.b = 2;
$scope.c = 4;
// 相等性觀察
$scope.$watchGroup(['a', 'b'], function(newValue, oldValue) {
if ($newValue != oldValue) {
$scope.obj.c = $scope.obj.a * $scope.obj.b;
}
});
3.$watchCollection - Collection Watch
這個是對集合元素的觀測:
- 用于觀測數(shù)組
- 檢測數(shù)組的變化(比如添加或刪除元素)
- 不檢測數(shù)組item的變化(比如數(shù)組的元素為對象,對象中的內(nèi)容發(fā)生變化,這并不再監(jiān)察范圍內(nèi), 如果希望監(jiān)察這種變化,可以添加
true, 同$watch)
$scope.emps = [
{ empId: 1001, empName: 'James'},
{ empId: 1002, empName: 'Kobe'},
{ empId: 1003, empName: 'Durant'},
{ empId: 1004, empName: 'Jordan'}
];
// 如果emps數(shù)組增加元素或刪除元素,則會調(diào)用watch listener
// 如果 '$scope.emps[2].empName="Harden"' 并不會調(diào)用watch listener
$scope.$watchCollection('emps', function(newValue, oldValue) {
//...
})
如果希望 '$scope.emps[2].empName="Harden"' 內(nèi)容的變化調(diào)用watch listener, 則可以添加 true, 變?yōu)?strong>相等性監(jiān)察
$scope.$watchCollection('emps', function(newValue, oldValue) {
//...
}, true)
示例:
// html
<div ng-controller="myCtrl">
a = {{a}} <input ng-model="a" ng-change="updateC()" />
</div>
// js
app.controller('myCtrl', function($scope) {
$scope.a = 10;
$scope.c = 1;
$scope.updateC = function() {
$scope.c = $scope.a * 2
}
$scope.watch('c', function(newValue, oldValue) {
if (newValue != oldValue) {
console.log('C updated to ' + newValue)
}
})
})
// 當我們改變 'a' 的值的時候,比如10, 'ng-change' 將調(diào)用 'updateC()'函數(shù)
// 從而改變c的值
// 然后c被檢測到發(fā)生變化,watch listener調(diào)用
// 控制臺顯示 'C updated to 20'
值得注意的是, ng-change 指令的用法, 當a的值發(fā)生變化時,將自動的調(diào)用其綁定的函數(shù)
digest process/loop
通過上面的watch,我們知道,作用域中的可能存在watchers, 這些watcher都存在各個作用域中的 watch list中,如下圖:

而 digest process 簡單的來講就是: 負責遍歷整個watch list,查看變化(也稱之為 dirty-checking(臟值檢查)),有如下特點:
- 被觀察的變量有變化,則更新變量,如果存在
watch listener, 則執(zhí)行watch listener - 追蹤變化,告知Angular更新DOM
- Digest Process 完成之后更新DOM
- Digest Process 運行在
Angular Context中,這是angularjs運行時環(huán)境,這個環(huán)境建立在Javascript Context上
如下圖:

舉個例子: watch list中的 a 變量,檢測到發(fā)生了變化:
- AngularJS 進行第1次遍歷,發(fā)現(xiàn)a有變化,更新a的值
- a如果存在watch listener, 則執(zhí)行這個函數(shù)
- AngularJS再次進行遍歷,查看是否又有變化,如果沒有變化,則Digest process完成,更新DOM;如果又發(fā)現(xiàn)變化,則再次進行遍歷,直到?jīng)]有任何變化
如下圖所示:

從圖中可以看出:
- AngularJS Digest process 至少遍歷2次, 最多遍歷10次(超過10次拋出錯誤)
我們可以使用 $rootScope.watch的方法來查看初始次數(shù):
$rootScope.watch(function() {
console.log('Digest process start');
})
// 在控制臺中我們可以看到
Digest process start
Digest process start
// 這個過程執(zhí)行了2次
digest process 完整過程
digest process并不會定時的觸發(fā),而是通過下圖中的一些條件引起而觸發(fā):

從圖中可以看出,其中幾個觸發(fā)條件為(在AngularJS context中):
- DOM 事件(ng-click等)
- Ajax 請求($http等)
- Timer 回調(diào)($timeout $interval)
- 瀏覽器地址發(fā)生變化
- 手動的調(diào)用(
$apply$digest)
Angularjs 也正是通過上面這些情形當作橋梁和瀏覽器產(chǎn)生關聯(lián)的
和angular不相關的DOM事件,比如 onclick, 并不會觸發(fā)digest process,而 ng-click 則會。
Angular context位于Javascript Context 中,瀏覽器的一些行為會影響到Angular context中的一些觸發(fā)條件,從而觸發(fā)digest process
$apply && $digest
這2個方法用于手動觸發(fā)digest process:
- 主要用于當作用域變量在Angular Context 之外(比如使用jquery 事件, jquery ajax等)被修改,而 UI 需要刷新數(shù)據(jù)綁定
-
$apply本質(zhì)上是調(diào)用$digest - 使用方法:
$scope.$apply()|$scope.$digest()

$apply
這個啟動digest process的特點:
- 永遠從
RootScope開始啟動digest process - ng-click, $timeout, $http(ajax) 等操作,背后都會調(diào)用$apply
$digest
這個和$apply的區(qū)別:
- 從 當前作用域 (包括子作用域和嵌套作用域) 啟動 digest process
- 不從RootScope或父作用域啟動
- 也能夠從RootScope開始東東digest process(這就相當于$apply了)
示例:
// html
<div ng-controller="MyCtrl" id="sum">
a: {{a}} <input ng-model="a" />
b: {} <input ng-model="b" />
<hr />
<button ng-click="getSum()">ng-click求和</button> // 這里使用ng-click,自動觸發(fā)digest process
<button ng-click="btnClick()">外部事件求和</button> // 這里使用外部DOM事件求和, 需要手動的觸發(fā)digest process
<hr />
總和為: {{s}}
</div>
// js
app.controller('MyCtrl', function($scope) {
$scope.a = 10;
$scope.b = 10;
$scope.s = 0;
$scope.getSum = function() {
$scope.s = parseInt($scope.a, 10) + parseInt($scope.b, 10);
}
})
// Angular context 外部邏輯
var btnClick = function() {
var sumDiv = document.getElementById('sum');
// 找出該作用域 這里的 '$scope' 可以為任何值
var $scope = angular.element(sumDiv).scope();
$scope.s = parseInt($scope.a, 10) + parseInt($scope.b, 10);
}
僅僅這樣寫,點擊 'ng-click求和' 將得到正確的值,但是點擊 '外部事件求和' 按鈕,雖然s的值會更新,但是DOM并未更新,這是我們需要使用 $scope.$apply() 手動的更新
var btnClick = function() {
var sumDiv = document.getElementById('sum');
var $scope = angular.element(sumDiv).scope();
$scope.s = parseInt($scope.a, 10) + parseInt($scope.b, 10);
$scope.$apply(); // 手動的啟動digest process
}
$scope.$apply() 也可以添加一個函數(shù),上面可以寫為:
var btnClick = function() {
var sumDiv = document.getElementById('sum');
var $scope = angular.element(sumDiv).scope();
// 使用函數(shù)的形式
$scope.$apply(function() {
$scope.s = parseInt($scope.a, 10) + parseInt($scope.b, 10);
});
}
總結(jié)
這一章主要有如下內(nèi)容:
- AngularJS的監(jiān)察機制:watch list, watch 的分類:
$watch,$watchGroup,$watchCollection - digest process的機制
- ng-change 指令,當綁定的變量發(fā)生變化時, 調(diào)用相應的函數(shù)
- Angular Context 的概念
- 觸發(fā)digest process的條件
- 手動觸發(fā)digest process:
$apply(),$digest(), 兩者之間的差異主要在于digest process開始的地方:$apply()從rootScope開始,而$digest()從當前scope開始,這對某些情況是有差別的