AngularJs 1.X的变化检测
双向绑定
AngularJs中引入了双向绑定机制
首先看一下模型和DOM进行双向绑定的代码片段
// view
<div>{{eggs}}</div>
// controller
app.controller('getEggController', [
'$scope',
function ($scope) {
this._init = function () {
$scope.eggs = 'myEgg';
};
this._init()
}]);
在该代码片段中, controller中的模型变量$scope.eggs与模型中{{eggs}}双向绑定,当controller中的$scope.eggs发生变化时,模板中的{{eggs}}同事会变化,相应的模板中的{{eggs}}发生变化时,controller中的$scope.eggs也会变化。这种双向绑定的原理是什么呢? AngularJs框架是怎样实现这种双向绑定机制的呢?
原理:
AngularJs会为你再scope模型上设置一个watcher, 它用来在模型数据发生变化时更新view,这里用到的watcher和我们会在angularJs中设置的watcher是一样的。即:
$scope.$watch('eggs', function(newValue, oldValue) {
//update the DOM with newValue
});
$watch原理
传入到$watch()中的第二个参数是一个回调函数,该函数在eggs的值发生变化的时候会被调用。那么AngularJS是如何知道什么时候要调用这个回调函数呢?换句话说,AngularJS是如何知晓eggs发生了变化,才调用了对应的回调函数呢?
AngularJs会周期性的运行一个函数$digest检查scope模型中的数据是否发生了变化,如果模型发生了变化,那么$watch中的回调函数就会被调用。 但是实际上angularJs并没有直接调用$digest(), 而是调用$scope.apply()。$scope.apply()会调用$rootScope.$digest()。因此,一轮$digest循环在$rootScope开始,随后会访问到所有的children scope中的watchers。
$digest原理
当$digest()被调用时,angular会遍历当前scope,以及children scope上的所有$watch, 如果watched value发生了变化,则运行回调函数,如果watched value没有变化,则执行下一个watcher。
当一个$digest循环运行时,watchers会被执行来检查scope中的models是否发生了变化。如果发生了变化,那么相应的listener函数就会被执行。这涉及到一个重要的问题。如果listener函数本身会修改一个scope model呢?
答案是$digest循环不会只运行一次。在当前的一次循环结束后,它会再执行一次循环用来检查是否有models发生了变化。这就是脏检查(Dirty Checking),它用来处理在listener函数被执行时可能引起的model变化。因此,$digest循环会持续运行直到model不再发生变化,或者$digest循环的次数达到了10次。因此,尽可能地不要在listener函数中修改model。
Note:
$digest循环最少也会运行两次,即使在listener函数中并没有改变任何model。正如上面讨论的那样,它会多运行一次来确保models没有变化。
$digest会在什么情况被触发呢? 一般当发生了一下Angular Context的事件的时候会触发$digest, 比如:
- DOM Events(eg. ng-click etc)
- Ajax的回调 (eg. $http etc)
- Timer with callbacks(eg. $timeout etc)
- 直接调用 $apply, $digest.
而对于普通的非angular上下文的异步事件,比如onclick, setTimeout等则不会触发$digest.因此对于这些非angular上下文的异步事件,要想进行模型的更新,则需要手动调用$apply手动进行模型的更新.
$scop.apply()
$scope.$apply()会自动地调用$rootScope.$digest()。$apply()方法有两种形式。第一种会接受一个function作为参数,执行该function并且触发一轮$digest循环。第二种会不接受任何参数,只是触发一轮$digest循环。而在手动使用 的过程中,推荐使用第一种方式,讲一个函数包裹在$scope.$apply()中,因为这种方法会自动的添加try....catch语句进行错误处理。
比如,对于以下语句:
$scope.$apply(doSomething)
本质上等同于:
doSomething();
$scope.digest();
下面来看一个例子
<!DOCTYPE html>
<html lang="en" ng-app="myApp">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<script src="https://code.angularjs.org/1.2.9/angular.min.js"></script>
<script>
angular.module('myApp', [])
.run(function($rootScope){
var $scope = $rootScope;
$scope.$watch('enabled', function(val) {
console.log('You are now: ' + (val ? 'enabled' : 'disabled'));
});
$scope.enabled = true;
$scope.enabled = false;
$scope.enabled = 1;
})
</script>
<body>
<p ng-if="enabled">I am here because I'm enabled</p>
<button ng-click="enabled=!enabled">Click Me!</button>
</body>
</html>
运行该代码,可以看到浏览器里面console打出的结果是 "You are now: enabled"。
但是你也许会疑惑,对于scope上的模型enabled明明进行了三次赋值,为什么只打出了一条log?
这是因为当$scope.enabled被第一次赋值初始化时,digest循环只会执行一次,若想要log语句三次都被执行,则要手动去调用$scope.apply()方法。 代码如下:
<!DOCTYPE html>
<html lang="en" ng-app="myApp">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<script src="https://code.angularjs.org/1.2.9/angular.min.js"></script>
<script>
angular.module('myApp', [])
.run(function($rootScope){
var $scope = $rootScope;
$scope.$watch('enabled', function(val) {
console.log('You are now: ' + (val ? 'enabled' : 'disabled'));
});
$scope.$apply(function() {
$scope.enabled = true;
});
$scope.$apply(function() {
$scope.enabled = false;
});
$scope.$apply(function() {
$scope.enabled = 1;
});
})
</script>
<body>
<p ng-if="enabled">I am here because I'm enabled</p>
<button ng-click="enabled=!enabled">Click Me!</button>
</body>
</html>
这也就说明了,在controller里进行初始赋值时如果不手动调用$scope.apply(),对于$scope上的模型的赋值操作只有第一次才会生效。
补充: $apply 和 $digest的一些区别:
参考文章
https://www.thinkful.com/projects/understanding-the-digest-cycle-528/
http://blog.csdn.net/dm_vincent/article/details/38705099