AngularJS双向数据绑定和脏检测机制

Posted by Eleven on 2017-08-30

前端面试AngularJS的时候会经常问到"AngularJS如何实现双向数据绑定?甚至会让你手写$digest代码.

虽然看了很多别的博客,但被问到这个问题的时候我还是很虚.后来发现我为什么那么虚,因为我没有写过,回归到代码本身的时候才不虚。

所以我参考了别人的博客和自己以前写AngularJS的经验,写下这篇博客。

什么时候触发脏检查机制?

  • UI事件(例如click事件等)
  • ajax请求
  • timeout延时函数

什么是双向数据绑定?

  • 界面的操作(UI事件,ajax请求,timeout延时函数)能实事反应到数据(View–>Model)
  • 数据的更改也能在界面呈现(Model–>View)

什么是脏检查?

实现从Model到View数据绑定的机制(实现数据的更改也能在界面呈现的机制),所以说界面的操作(UI事件,ajax请求,timeout延时函数)是触发脏检查机制。

手写AngularJS脏检测机制

  • dirty-checking.html
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>手写脏检查机制</title>
<style type="text/css">
button {
height: 60px;
width: 100px;
}
p {
margin-left: 20px;
}
</style>
</head>
<body>
<div>
<button ng-click="increaseSprite">增加</button>
<button ng-click="decreaseSprite">减少</button>
雪碧:<span ng-bind="sprite"></span>
</div>
<div>
<button ng-click="increaseCola">增加</button>
<button ng-click="decreaseCola">减少</button>
雪碧:<span ng-bind="cola"></span>
</div>
<div>合计=<span ng-bind="sum"></span></div>
<script src="./dirty-checked.js"></script>
</body>
</html>
  • dirty-checking.js
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/**
* Created by eleven on 2017/8/30.
*/
window.onload = function () {
'use strict'
var scope = {
increaseSprite: function () {
this.sprite++;
},
decreaseSprite: function () {
this.sprite--;
},
increaseCola: function () {
this.cola++;
},
decreaseCola: function () {
this.cola--;
},
cola: 0,
sprite: 0,
price: 3
}
function Scope() {
this.$$watchList = [];
}
Scope.prototype.$watch = function (name, getNewValue, listener) {
var watch = {
name: name, //当前watch对象的key值,和ng-bind的值是一致
getNewValue: getNewValue, //得到新的值
listener: listener || function () {} //当数据改变时需要执行的回调函数
};
this.$$watchList.push(watch);
}
Scope.prototype.$digest = function () {
var dirty = true;
var checkTimes = 0;
while(dirty){
dirty = this.$$digestOnce();
checkTimes++;
if(checkTimes > 10 && true){
throw new Error("检测超过10次");
}
};
}
Scope.prototype.$$digestOnce = function () {
var dirty;
var list = this.$$watchList;
for(var i = 0, l = list.length; i <l; i++){
var watch = list[i];
var newValue = watch.getNewValue();
var oldValue = watch.last;
if(newValue !== oldValue){
watch.listener(newValue, oldValue);
//因为listener操作,已经检测过的数据可能变脏
dirty = true;
}else{
dirty = false;
}
watch.last = newValue;
}
return dirty;
}
//将数据绑定到UI时,就会执行如下代码,在watchList中插入watch对象
var $scope = new Scope();
$scope.$watch('sprite',function () {
$scope.sprite = scope.sprite;
return $scope[this.name];
},function (newValue, oldValue) {
console.log("sprite: newValue:" + newValue + '-------' + "oldValue" + oldValue);
});
$scope.$watch('cola',function () {
$scope.cola = scope.cola;
return $scope[this.name];
},function (newValue, oldValue) {
console.log("cola: newValue:" + newValue + '-------' + "oldValue" + oldValue);
});
$scope.$watch('sum',function () {
$scope.sum = (scope.cola + scope.sprite)*scope.price;
return $scope[this.name];
},function (newValue, oldValue) {
console.log("sum: newValue:" + newValue + '-------' + "oldValue" + oldValue);
});
function bind() {
var list = document.querySelectorAll('[ng-click]');
for(var i = 0, l = list.length; i < l; i++){
list[i].onclick = (function () {
return function () {
var func = this.getAttribute('ng-click');
scope[func]();
apply();
}
})()
}
}
function apply() {
//脏检测(检查一遍数据的变化,如果数据和上次的值有变化,则执行这个值(注册时)对应的callback(框架中),)
$scope.$digest();
var list = document.querySelectorAll('[ng-bind]');
for(var i = 0, l = list.length; i < l; i++){
var bindData = list[i].getAttribute('ng-bind');
list[i].innerHTML = $scope[bindData];
}
}
bind();
apply();
}

脏检测的优缺点【todo】

Object.defineProperty()中使用setter/getter钩子实现双向数据绑定不适用于循环大量setter的场景。

为什么要进行脏检测?

比如说当你click事件,造成数据变化时,你不知道还有哪些数据因此受影响,所以必须循环一次注册到watchlist中的watch,当然watch的callback函数,也会造成数据变化,所以可能会一直在检测中,造成死循环,所以AngularJS会限制检测的次数,至少检测两次(也是两次循环watchlsit)