文章目录
  1. 1. 小栗子
  2. 2. 原理
    1. 2.1. 视图到模型
    2. 2.2. 模型到视图
  3. 3. 简单实现
  4. 4. 参考文献

双向数据绑定是AngularJS最主要的特性之一,这个特性使得我们操作DOM变得更加容易,也使得AngularJS更适应于实时变化的场景,如实时监控。刚开始在使用AngularJs 时这个特性让我简直不能再惊讶,所以去看看实现咯。

小栗子

首先我们先来考虑一个简单的场景,就是我们可以输入数值,来控制一个矩形的长度。在这里,数据与表现的关系是:

1
2
3
4
5
> 长度数值保存在变量中
> 变量显示于某个 input 中
> 变量的值即是矩形的长度
> input 中的值变化时,变量也要变化
> input 中的值变化时,矩形的长度也要变化

所以我们在不使用ng 时,应该先找到input,然后获取值,给input添加一个chang事件,将这个值实时变化更改给矩形框的长度,然后再更改矩形框的DOM显示。所以在正常情况下,必须使用的响应的函数(如change)来进行同步更改,不然并不能进行实时更改相应值。 而在 ng中,可以实现如下:

1
2
3
4
5
6
7
8
9
10
11
<div ng-controller="TestCtrl">
<div style="width: 10px; height: 10px; background-color: red" ng-style="style">
</div>
<input type="text" name="width" ng-model="style.width" />
</div>
<script type="text/javascript" charset="utf-8">
var TestCtrl = function($scope){
$scope.style = {width: 100 + 'px'};
}
angular.bootstrap(document.documentElement);
</script>

在这个例子中,我们并未直接调用chang函数,而是使用了ng-model 与ng-style这两个指令,并非是这两个指令有什么神奇的地方,而是AngularJS在实现时给ng-model 绑定了事件来进行数据处理,所以才有了不用我们自己再写change函数。

原理

在AngularJS中扩展了事件循环,生成一个angular context的运行环境。在scope()函数的原型上实现的$watch方法中有一个$$watcher 队列。每当绑定一些东西到DOM上时就会在$$watcher这个队列里添加一个 $$watcher。那这写$watch是什么时候生成的呢? 当我们的模版加载完毕时,也就是在linking阶段,Angular解释器会寻找每个directive,然后生成每个需要的$watch。

在scope()的原型上还有一个方法是$digest ,这个方法主要作用是一个循环。当浏览器接收到可以被angular context处理的事件时,$digest循环就会触发。这个循环是由两个更小的循环组合起来的。一个处理evalAsync队列,另一个处理$$watcher队列。这个是处理什么的呢?$digest将会遍历我们的$watch,这个循环会将$$watchers中的每一个值都进行检查,检查当前值和原来值是否相同,这就是 dirty-checking(脏值检查)。当值检查完后,会继续看这个队列是否有再更新过,如果有至少一个更新过,这个循环会再次触发,知道队列中的所有值都不再变化。记住如果循环超过10次的话,它将会抛出一个异常,防止无限循环。 当$digest循环结束时,DOM相应地变化。

每一个进入angular context的事件都会执行一个$digest循环,也就是说每次我们输入一个字母循环都会检查整个页面的所有$watch。那么事件怎么样就可以进入angular context 中呢?这个是通过$apply 来进行控制的。当事件触发时,调用$apply,则会进入angular contxt ,若是没有调用$apply 则不会进入。
在调用$apply之后 才会调用$digest.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$apply: function(expr) {
try {
beginPhase('$apply');
return this.$eval(expr);
} catch (e) {
$exceptionHandler(e);
} finally {
clearPhase();
try {
$rootScope.$digest();
} catch (e) {
$exceptionHandler(e);
throw e;
}
}
}

这主要就是双向数据绑定主要用到的方法: $watch,$digest,$apply。

下面我们来具体分析一下视图到模型 和模型到视图这两个单方面的实现。

视图到模型

先看下面这个栗子 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<body ng-controller="MainCtrl">
<clickable foo="foo" bar="bar"></clickable>
<hr /> {{ hello }} <button ng-click="setHello()">Change hello</button>
<script type="text/javascript">
app = angular.module('app', []);
app.controller('MainCtrl', function($scope) {
$scope.foo = 0;
$scope.bar = 0;
$scope.hello = "Hello";
$scope.setHello = function() {
$scope.hello = "World";
};
});
app.directive('clickable', function() {
return {
restrict: "E",
scope: {
foo: '=',
bar: '='
},
template: '<ul style="background-color: lightblue"><li>{{foo}}</li><li>{{bar}}</li></ul>',
link: function(scope, element, attrs) {
element.bind('click', function() {
scope.foo++;
scope.bar++;
});
}
}
});
</script>
</body>

在这个小栗子中,点击蓝色列表并不会使得DOM中的显示发生变化,但在点击change Hello这个按钮的时候会使得蓝色列表同时发生变化,这表明在点击蓝色按钮的时候确实会使得模型上的数据发生更改,即在DOM元素上绑定事件,就可以产生视图到模型的绑定。

在这个例子中,{ { } }指令将foo,bar 放在$$watcher队列中了,所以不需要自己手动添加了,我们也可以使用$watch 来检测自己的想检测的变量。

1
2
3
4
5
6
7
8
9
10
11
<body ng-controller="MainCtrl">
<input ng-model="name" />
Name updated: {{updated}} times.
</body>
app.controller('MainCtrl', function($scope) {
$scope.name = "Angular";
$scope.updated = -1;
$scope.$watch('name', function() {
$scope.updated++;
});
});

但 检测引用类型时,添加$watch 函数的第三个参数,若是为true,则表示的是我们比较的是对象的值而不是引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<body ng-controller="MainCtrl">
<input ng-model="user.name" />
Name updated: {{updated}} times.
</body>
app.controller('MainCtrl', function($scope) {
$scope.user = { name: "Fox" };
$scope.updated = 0;
$scope.$watch('user', function(newValue, oldValue) {
if (newValue === oldValue) { return; }
$scope.updated++;
}, true);
});

那为什么没有在点击蓝色列表时发生DOM上的同步更改呢? 而在点击change Hello这个按钮的时候会使得蓝色列表同时发生变化?这是因为我们还没给蓝色列表添加模型到视图上的绑定。但是ng-click 是angular实现的,会自动调用$digest循环$$watcher队列,更新DOM。那怎么样给蓝色列表添加模型到视图的绑定呢?

模型到视图

将上面的指令实现改成下面这样,就可以实现模型到视图的绑定了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
app.directive('clickable', function() {
return {
restrict: "E",
scope: {
foo: '=',
bar: '='
},
template: '<ul style="background-color: lightblue"><li>{{foo}}</li><li>{{bar}}</li></ul>',
link: function(scope, element, attrs) {
element.bind('click', function() {
scope.$apply(function() {
scope.foo++;
scope.bar++;
});
})
}
}
});

调用$apply会强制一次$digest循环(除非当前正在执行循环,这种情况下会抛出一个异常,这是我们不需要在那里执行$apply的标志)。

简单实现

其实整个双向数据绑定过程就是 :有一个队列($$watcher)用来存放想绑定的数据,有一个循环($digest)来检查这个队列中的值是否脏了(dirty-checking),有一个启动函数($apply)来决定什么时候来启动这个循环。所以我们自己也可以实现这样的一个双向绑定。只是我们自己的环境比较简单,也用不到angular context ,所以 用不到 $apply;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<body>
<input type="text" name="name" value="">
<script type="text/javascript">
var Scope = function() {
this.$$watchers = [];
};
Scope.prototype.$watch = function(watchExp, listener) {
this.$$watchers.push({
watchExp: watchExp,
listener: listener || function() {}
});
};
Scope.prototype.$digest = function() {
var dirty;
do {
dirty = false;
for (var i = 0; i < this.$$watchers.length; i++) {
var newValue = this.$$watchers[i].watchExp(),
oldValue = this.$$watchers[i].last;
if (oldValue !== newValue) {
this.$$watchers[i].listener(newValue, oldValue);
dirty = true;
this.$$watchers[i].last = newValue;
}
}
} while (dirty);
};
var $scope = new Scope();
$scope.name = 'Ryan';
var element = document.querySelectorAll('input');
element[0].onkeyup = function() {
$scope.name = element[0].value;
$scope.$digest();
};
$scope.$watch(function() {
return $scope.name;
}, function(newValue, oldValue) {
console.log('Input value updated - it is now ' + newValue);
element[0].value = $scope.name;
});
var updateScopeValue = function updateScopeValue() {
$scope.name = 'Bob';
$scope.$digest();
};
</script>
</body>

参考文献

  1. 理解$watch ,$apply 和 $digest — 理解数据绑定过程
  2. AngularJS 数据双向绑定揭秘
文章目录
  1. 1. 小栗子
  2. 2. 原理
    1. 2.1. 视图到模型
    2. 2.2. 模型到视图
  3. 3. 简单实现
  4. 4. 参考文献