Angular Js 双向数据绑定原理

近期接手了公司的一个老项目,需要在原有项目的基础上做些改进。由于之前没用过angularjs,在刚开始的时候非常痛苦,在改代码的过程中需要一个一个指令的查。到了后期,虽然不需要一个一个指令的查,但对于angularjs也只是一知半解。比如下面这一段代码

1
2
3
4
5
6
7
8
9
10
$scope.safeApply = function(fn){
var phase = this.$root.$$phase;
if (phase == '$apply' || phase == '$digest') {
if (fn && ( typeof (fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
}

从这段代码看,它对的意思应该是,当phase满足以上条件的时候执行fn这个函数,否则执行scope.$apply(fn)。

对于这段代码,我有几个疑问
1
2
1. $$phase 是什么?
2. $apply的作用是什么?

对于第一个问题,简单的做一些描述,$$phase 是 angluar 内部使用的状态标志位,用于标识当前是否处于 digest 状态。

对于第二个问题,$apply的作用是什么?这个就涉及到angularjs的双向数据绑定的特性,现下做一个详细的探索。

双向数据绑定意味着,当视图(view)中的数据发生变化的时候,作用域下的数据模型(model)也会相应的更新。同样的,当数据模型(model)发生变化的时候,视图(view)也会相应的更新。那么,AngularJS是怎么做到的呢?当你写这样的一个表达式的时候( ),AngularJS在背后为这个数据模型(model)设置了一个监听器(watcher),正是由于这个监听器(watcher),无论何时当数据模型(model)发生变化的时候,视图(view)也会更新。

$watch

1
2
3
$scope.$watch('aModel', function(newValue, oldValue) {
//update the DOM with newValue
});

传入到$watch()中的第二个参数是一个回调函数,该函数在aModel的值发生变化的时候会被调用。当aModel发生变化的时候,这个回调函数会被调用来更新view这一点不难理解,但是,还存在一个很重要的问题!AngularJS是如何知道什么时候要调用这个回调函数呢?换句话说,AngularJS是如何知晓aModel发生了变化,才调用了对应的回调函数呢?它会周期性的运行一个函数来检查scope模型中的数据是否发生了变化吗?好吧,这就是$digest循环的用武之地了。

在$digest循环中,watchers会被触发。当一个watcher被触发时,AngularJS会检测scope模型,如果它发生了变化那么关联到该watcher的回调函数就会被调用。那么,下一个问题就是$digest循环是在什么时候以各种方式开始的?

在调用了$scope.$digest()后,$digest循环就开始了。假设你在一个ng-click指令对应的handler函数中更改了scope中的一条数据,此时AngularJS会自动地通过调用$digest()来触发一轮$digest循环。当$digest循环开始后,它会触发每个watcher。这些watchers会检查scope中的当前model值是否和上一次计算得到的model值不同。如果不同,那么对应的回调函数会被执行。调用该函数的结果,就是view中的表达式内容(译注:诸如)会被更新。除了ng-click指令,还有一些其它的built-in指令以及服务来让你更改models(比如ng-model,$timeout等)和自动触发一次$digest循环。

但是,又一个小问题,实例如下

1
2
3
<select id="uidSelect" class="sel" ng-model="sid" ng-init="" ng-options="user.name for user in users">
<option value="" default>-Select one-</option>
</select>

因为开始的时候users数组的数据是固定的浏览器渲染出来后值就是固定的,那么如果在这之后我们想往users里面添加数据这个下拉选项是不会有改变的。此时,我们可以通过调用 scope.$apply()实时刷新ui。

$apply() 方法的两种形式

1) 无参

1
2
3
4
5
element.bind('click', function() {
scope.foo++;
//if error
scope.$apply();
});

弊端:当我们使用这种形式的时候,如果在scope.$apply之前程序发生异常,那scope.$apply没有执行,界面就不会更新

2)有参

1
2
3
4
5
element.bind('click', function() {
scope.$apply(function() {
scope.foo++;
});
}) // 即使后面发生异常,数据还是会实时更新

总结:

  1. 只有在$scope变量绑定到页面上,才会创建 $watch
  2. $apply决定事件是否可以进入angular context
  3. $digest 循环检查model时最少两次,最多10次(多于10次抛出异常,防止无限检查)
  4. AngularJs自带的指令已经实现了$apply,所以不需要我们额外的编写
  5. 在自定义指令时,建议使用带function参数的$apply

参考文献