AngularJS 的脏检查机制

AngularJS的双向数据绑定采用的脏检查机制,所谓“脏检查”,就是检测到数据与之前不一致,有变化,这时候就认为数据是“脏”的,然后把不一致的数据变更为较新的值,直到没有新的数据变动。

通常UI事件、ajax请求后的数据处理 或者 timeout 延迟事件会触发这个脏检查这个过程的实现,主要是靠 $watch$digest 两个重要的函数。

$watch

每当有变量通过$scope对象和页面UI绑定,就会增加一个watch对象

1
2
3
4
5
6
7
8
9
10
watch = {
name:'', //当前的watch 对象 观测的数据名
getNewValue:function($scope){ //得到新值
...
return newValue;
},
listener:function(newValue,oldValue){ // 当数据发生改变时需要执行的操作
...
}
}

$watch函数会收集所有的watch对象到$$watchList数组中,然后等待$digest遍历$$watchList中的listener,用代码表示更为直观

1
2
3
4
5
6
7
8
9
$scope.prototype.$watch = function(name,getNewValue,listener){
var watch = {
name:name,
getNewValue : getNewValue,
listener : listener
};

this.$$watchList.push(watch);
}

挂载到$scope原型上是为了每个Scope实例上存储这些函数。

$digest

$digest函数的作用就是在触发脏检查的时候,遍历之前push到$$watchList中的watch.listener,用代码解释就是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
$scope.prototype.$digest = function(){
var list = this.$$watchList;
for(var i = 0,l= list.length;i++){
var watch = list[i];
var newValue = watch.getNewValue(this);
// 在第一次渲染界面,进行一个数据呈现.
var oldValue = watch.last;
if(newValue!=oldValue){
watch.listener(newValue,oldValue);
}
watch.last = newValue;
}
}

watch.last用于存储上一次的值,当newValue!=oldValue时,就认为数据上“脏”的,就会递归调用$digest,保证所有数据都一致。$digest至少会调用两次,才能确保数据是干净的,当然也不会无休止的递归下去,递归10次就会抛出异常了,这种情况说明业务数据过于复杂(或者代码特别渣渣),需要开发人员优化一下了。

Vue的数据劫持模式

vue的双向数据绑定,采用数据劫持模式,核心是Object.defineProperty,Vue3.0的核心要变成proxy了。

通过 Object.defineProperty() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty() 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化

Object.defineProperty

对于vue({data:{}}),data对象上的所有属性,都会被Object.defineProperty劫持,增加对应的gettersetter,结合发布订阅模式,就可以对数据检测赋值,这里说明一下数据劫持部分,代码如下:

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

function observe(data){
if(typeof data !== 'object'){//不是对象不进行数据劫持
return
}
return new Observe(data);
}

//将model->vm.data
function Observe(data){
for(let key in data){//遍历所有属性进行劫持
let val = data[key];
observe(val);//深入递归数据劫持exp:data:{a:{a:3},b:5}}
Object.defineProperty(data,key,{
enumerable: true,
get(){
return val//此时的val已经进行了数据劫持,exp:{a:3}
},
set(newVal){
if(newVal === val ){//值不变则返回
return
}
val = newVal;
observe(newVal);//新赋的值也必须进行数据劫持
}
}
}
}

Object.defineProperty缺点是无法监听数组变化,vue是单独对数组进行了常用方法的hack。

proxy

Proxy在ES2015规范中被正式发布,它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写,我们可以这样认为,Proxy是Object.defineProperty的全方位加强版,proxy可以监听整个对象,无论对象内部的属性是数组还是新对象。

proxy 劫持的简单例子:

1
2
3
4
5
6
7
8
9
10
const newObj = new Proxy(obj, {
get: function(target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log(target, key, value, receiver);
return Reflect.set(target, key, value, receiver);
},
});

被劫持的对象会被添加对应的watcher,以此实现双向数据绑定。

参考文章: