watcher是Angular提供的一种对变量的观察机制,如果存在数据的绑定等,则在不同的作用域中都存在相应的 watch list
, 这些被观察的变量将在 digest process
(后面会详讲)中得到更新,这也称之为 脏值检查。
watch 和 watch listener
在 $scope
(和$rootScope
) 对象上存在 $watch
函数,我们可以手动的添加watch listener, 当被观察的变量被检测到发生变化时,变量被更新,然后angularjs自动调用 watch listener.
// 观察 'a' 变量的变化
// 后面的回调函数就是 watch listener, 2个参数: newValue oldValue
$scope.$watch('a', function(newValue, oldValue) {
if (newValue != oldValue) {
$scope.b = $scope.a * 2
}
})
watches的类型
1.$watch - Reference watch 引用类型的观测
$watch
是一种引用类型的观察,只有当引用的地址发生变化时才触发,上面的代码使用原始类型,当值变化时,相应的引用也会发生变化。下面例子中 obj
是一个对象,只有 obj
引用的地址发生变化时,才会调用watch listener, 因此当 a, b, c的值发生变化时,obj引用的地址并不会发生改变,因此不会调用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', 则观察的类型将变为相等性观察(也可称为deep watch)。这样对象的成员发生变化时,会自动调用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
对多个变量同时进行监测,这可以说是一种简便的写法
$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
这个是对集合元素的观测:
- 用于观测数组
- 检测数组的变化(比如添加或删除元素)
- 不检测数组item的变化(比如数组的元素为对象,对象中的内容发生变化,这并不再监察范围内, 如果希望监察这种变化,可以添加
true
, 同$watch)
$scope.emps = [
{ empId: 1001, empName: 'James'},
{ empId: 1002, empName: 'Kobe'},
{ empId: 1003, empName: 'Durant'},
{ empId: 1004, empName: 'Jordan'}
];
// 如果emps数组增加元素或删除元素,则会调用watch listener
// 如果 '$scope.emps[2].empName="Harden"' 并不会调用watch listener
$scope.$watchCollection('emps', function(newValue, oldValue) {
//...
})
如果希望 '$scope.emps[2].empName="Harden"' 内容的变化调用watch listener, 则可以添加 true
, 变为相等性监察
$scope.$watchCollection('emps', function(newValue, oldValue) {
//...
}, true)
示例:
// html
a = {{a}}
// 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' 将调用 'updateC()'函数
// 从而改变c的值
// 然后c被检测到发生变化,watch listener调用
// 控制台显示 'C updated to 20'
值得注意的是, ng-change
指令的用法, 当a的值发生变化时,将自动的调用其绑定的函数
digest process/loop
通过上面的watch,我们知道,作用域中的可能存在watchers, 这些watcher都存在各个作用域中的 watch list
中,如下图:
而 digest process 简单的来讲就是: 负责遍历整个watch list,查看变化(也称之为 dirty-checking(脏值检查)
),有如下特点:
- 被观察的变量有变化,则更新变量,如果存在
watch listener
, 则执行watch listener - 追踪变化,告知Angular更新DOM
- Digest Process 完成之后更新DOM
- Digest Process 运行在
Angular Context
中,这是angularjs运行时环境,这个环境建立在Javascript Context
上
如下图:
举个例子: watch list中的 a
变量,检测到发生了变化:
- AngularJS 进行第1次遍历,发现a有变化,更新a的值
- a如果存在watch listener, 则执行这个函数
- AngularJS再次进行遍历,查看是否又有变化,如果没有变化,则Digest process完成,更新DOM;如果又发现变化,则再次进行遍历,直到没有任何变化
如下图所示:
从图中可以看出:
- AngularJS Digest process 至少遍历2次, 最多遍历10次(超过10次抛出错误)
我们可以使用 $rootScope.watch
的方法来查看初始次数:
$rootScope.watch(function() {
console.log('Digest process start');
})
// 在控制台中我们可以看到
Digest process start
Digest process start
// 这个过程执行了2次
digest process 完整过程
digest process并不会定时的触发,而是通过下图中的一些条件引起而触发:
从图中可以看出,其中几个触发条件为(在AngularJS context中):
- DOM 事件(ng-click等)
- Ajax 请求($http等)
- Timer 回调($timeout $interval)
- 浏览器地址发生变化
- 手动的调用(
$apply
$digest
)
Angularjs 也正是通过上面这些情形当作桥梁和浏览器产生关联的
和angular不相关的DOM事件,比如 onclick, 并不会触发digest process,而 ng-click 则会。
Angular context位于Javascript Context 中,浏览器的一些行为会影响到Angular context中的一些触发条件,从而触发digest process
$apply && $digest
这2个方法用于手动触发digest process:
- 主要用于当作用域变量在Angular Context 之外(比如使用jquery 事件, jquery ajax等)被修改,而 UI 需要刷新数据绑定
-
$apply
本质上是调用$digest
- 使用方法:
$scope.$apply()
|$scope.$digest()
$apply
这个启动digest process的特点:
- 永远从
RootScope
开始启动digest process - ng-click, $timeout, $http(ajax) 等操作,背后都会调用$apply
$digest
这个和$apply的区别:
- 从 当前作用域 (包括子作用域和嵌套作用域) 启动 digest process
- 不从RootScope或父作用域启动
- 也能够从RootScope开始东东digest process(这就相当于$apply了)
示例:
// html
a: {{a}}
b: {{b}}
// 这里使用ng-click,自动触发digest process
// 这里使用外部DOM事件求和, 需要手动的触发digest process
总和为: {{s}}
// 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()
也可以添加一个函数,上面可以写为:
var btnClick = function() {
var sumDiv = document.getElementById('sum');
var $scope = angular.element(sumDiv).scope();
// 使用函数的形式
$scope.$apply(function() {
$scope.s = parseInt($scope.a, 10) + parseInt($scope.b, 10);
});
}
总结
这一章主要有如下内容:
- AngularJS的监察机制:watch list, watch 的分类:
$watch
,$watchGroup
,$watchCollection
- digest process的机制
- ng-change 指令,当绑定的变量发生变化时, 调用相应的函数
- Angular Context 的概念
- 触发digest process的条件
- 手动触发digest process:
$apply()
,$digest()
, 两者之间的差异主要在于digest process开始的地方:$apply()
从rootScope开始,而$digest()
从当前scope开始,这对某些情况是有差别的