一 简介
AngularJS提供了一个非常酷的特性叫做双向数据绑定(Two-way Data Binding),这个特性大大简化了我们的代码编写方式。数据绑定意味着当View中有任何数据发生了变化,那么这个变化也会自动地反馈到scope的数据上,也即意味着scope模型会自动地更新。类似地,当scope模型发生变化时,view中的数据也会更新到最新的值。那么AngularJS是如何做到这一点的呢?当你写下表达式如{{ aModel }}时,AngularJS在幕后会为你在scope模型上设置一个watcher,它用来在数据发生变化的时候更新view。
二 $watch
1.什么是$watch
$scope对象上的$watch方法会给Angular事件循环内的每个$digest调用装配一个脏值检查,如果在表达式上检测到变化,Angular总是会返回$digest循环。
也就是说,$watch代表的就是对数据源的监听,当数据源发生变化,就会触发第二个参数的回调函数。
2.使用
$watch函数本身接受两个必要参数和一个可选的参数:
$scope.$watch(‘aModel’, function(newValue, oldValue) {
//update the DOM with newValue
},true);
第一个参数:可以是一个作用域对象的属性,或者是一个函数,在$digest循环中的每个$digest调用都会涉及到它。如果是一个字符串,Angular会在$scope上下文中对它求值。
第二个参数:作为回调的监听器函数,它智慧在第一个参数的当前值与先前值不相等时调用。
第三个参数:true/false,默认为false,主要用于第一个参数为引用型的情况下。
3.举例:
<body>
<input ng-model=‘name‘ type=‘text‘/>
<div>change count: {{count}}</div>
<script>
angular.module(‘myApp‘,[])
.run([‘$rootScope‘,function($rootScope){
$rootScope.count = 0;
$rootScope.name = ‘hcc‘;
$rootScope.$watch(‘name‘,function(){
$rootScope.count++;
})
}]);
</script>
</body>
用$watch来对$rootScope中的name进行监视,并在它发生变化的时候将$rootScope中的count属性增加1。因此,每当我们对name进行一次修改时,下面显示的count数字就会增加1。
然而,我们在实际运用中常常不只是对一个原始类型的属性进行监视,还可能对集合进行监视。对于原始类型,如果我们使用了一个赋值操作,则这个原始类型变量会“真正的”被进行一次复制,然而对于引用类型,在进行赋值时,仅仅是将赋值的变量指向了这个引用类型。因此如果要对一个引用类型,尤其是在实际运用中常见的对象数组进行监视时,情况就不一样了。
<body>
<div hg-repeat=‘item in items‘>
<input ng-model=‘item.a‘/><span>{{item.a}}</span>
</div>
<div>change count: {{count}}</div>
<script>
angular.module(‘myApp‘,[])
.run([‘$rootScope‘,function($rootScope){
$rootScope.count = 0;
$rootScope.items = [
{ "a": 1 },
{ "a": 2 },
{ "a": 3 },
{ "a": 4 }
]
$rootScope.$watch(‘items‘,function(){
$rootScope.count++;
},true)
}]);
</script>
</body>
在angular 1.1.4版本之后,添加了一个$watchCollection()方法来针对数组(也就是集合)进行监视,它的性能介于全等监视和引用监视二者之间,即它并不会对数组中每一项的属性进行监视,但是可以对数组的项目的增减做出反应。
在这里只需将$rootScope.$watch改成$rootScope.$watchCollection即可:
$rootScope.$watchCollection(‘items‘,function() {
$rootScope.count++;
})
对集合的操作,推荐使用这种方式。
三 $digest和$apply
1.在调用了$scope.$digest()后,$digest循环就开始了。假设你在一个ng-click指令对应的handler函数中更改了scope中的一条数据,此时AngularJS会自动地通过调用$digest()来触发一轮$digest循环。当$digest循环开始后,它会触发每个watcher。这些watchers会检查scope中的当前model值是否和上一次计算得到的model值不同。如果不同,那么对应的回调函数会被执行。调用该函数的结果,就是view中的表达式内容会被更新。
AngularJS并不直接调用$digest(),而是调用$scope.$apply(),后者会调用$rootScope.$digest()。因此,一轮$digest循环在$rootScope开始,随后会访问到所有的children scope中的watchers。
正常情况下,在angular上下文中,修改数据源就会自动触发。$apply只是把$digest做了一次封装,来提供手动触发,那么为什么需要手动触发呢。因为如果是不在angular上下文的情况下,如浏览器DOM事件,setTimeout执行,这种情况下,angular无法获取到事件,所以,通过apply来手动触发一下,在apply的参数中去修改数据源。
2.举例:
<body ng-app=“myApp”>
<div ng-controller=“MessageController”>
Delayed Message: {{message}}
</div>
</body>
angular.module(‘myApp’,[]).controller(‘MessageController’, function($scope) {
$scope.getMessage = function() {
setTimeout(function() {
$scope.message = ‘Fetched after 3 seconds‘;
console.log(‘message:’+$scope.message);
}, 2000);
}
$scope.getMessage();
});
通过运行这个例子,你会看到过了两秒钟之后,控制台确实会显示出已经更新的model,然而,view并没有更新。原因也许你已经知道了,就是我们忘了调用$apply()方法。因此,我们需要修改getMessage()。
angular.module(‘myApp’,[]).controller(‘MessageController’, function($scope) {
$scope.getMessage = function() {
setTimeout(function() {
$scope.$apply(function() {
//wrapped this within $apply
$scope.message = ‘Fetched after 3 seconds‘;
console.log(‘message:’ + $scope.message);
});
}, 2000);
}
$scope.getMessage();
});