vue双向绑定解析

vue双向绑定核心

数据劫持+发布订阅模式。

数据劫持

数据劫持的核心是Object.defineProperty()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
descriptor = {
enumerable:true, //该属性能被枚举(for...in和Object.keys())
configurable:true, //该属性描述符能被改变,同时也能删除
get:function(){
//todo 在使用该属性时增加监听者
return value;
},
set:function(newVal){
//todo 重新赋值,通知订阅者更新模板
value = newVal;
}
}

Object.defineProperty(obj,key,value,descriptor)

vue双向绑定主要组成部分

  1. Ovserver:主要的工作是递归地监听对象上的所有属性,在属性的值改变时,触发相应的watcher。
  2. Watcher:观察者。当监听的数据(触发set)修改时,执行相应的回调函数。(调用compiler更新模板)
  3. Dep:连接Observer和Watcher的桥梁,每一个Observer对应一个Dep,它内部维护一个数组,保存于该Observer相关的Watcher(当使用该值时,会把使用者存放到该Observer的Dep对象中)。
  4. Compiler:模板解析器,用于渲染数据。

data.png

一个简单的demo

Observer

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
function Observer(obj,key,value) {
var dep = new Dep();
//demo只处理object类型
if(Object.prototype.toString.call(value) === '[object Object]'){
Object.keys(value).forEach(function (key) {
//递归给对象的每个属性增加get、set属性
new Observer(value,key,value[key]);
})
}
Object.defineProperty(obj , key , {
enumerable:true,
configurable:true,
get:function () {
//该属性被引用,将引用者watcher加入dep(depend)数组中。
if(Dep.target){
dep.addSub(Dep.target);
}
return value;
},
set:function (newVal) {
//该属性被修改,通知dep中的watcher,触发watcher执行回调,触发diff,更新虚拟dom,从而更新模板
value = newVal;
dep.notify();
}
})
}

Dep

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Dep(){
//每一个observer属性内部都维护一个dep数组,用于存放与之依赖的watcher
this.subs = [];
this.addSub = function (watcher) {
this.subs.push(watcher);
console.log(this.subs);
}
//当observer属性的set触发时,notify函数用于触发watcher更新模板
this.notify = function () {
this.subs.forEach(function (watcher) {
watcher.update()
})
}
}

Watcher

1
2
3
4
5
6
7
8
9
10
//fn是数据变化后腰执行的回调函数,一般是获取数据渲染模板
function Watcher(fn) {
this.update = function () {
Dep.target = this;
fn();
Dep.target = null;
}
//默认执行一边update是为了在渲染模板过程中,调用对象的getter时建立两者之间的关系。
this.update();
}

同一时刻只有一个watcher处于激活状态,把当前watcher绑定在Dep.target(方便在Observer内获取)。回调结束后,销毁Dep.target。

html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Observer</title>
</head>
<body>
<div id="test"></div>
<script src="observer.js"></script>
<script>
var obj = {a:1,b:2,c:3}
Object.keys(obj).forEach(function (key) {
new Observer(obj,key,obj[key]);
});
console.log(obj);
new Watcher(function () {
document.querySelector("#test").innerHTML = obj.a;
})
</script>
</body>
</html>

总结Vue双向绑定实现

  1. Vue会为data中对象(处理Object [walk函数] 和Array [observeArray函数])的每个属性通过Object.defineProperty()添加一个get、set属性,将每个属性设置为可观察的(Observer),每个属性内部维护一个dep数组,这个数组存放着与这个属性有关的watcher对象,watcher对象可以理解为使用了某个observer属性的对象。
  2. 使用Observer属性时会触发该属性的get函数。调用get函数时,表示该属性被使用,这时就会调用dep.addsubs()把当前的watcher存放到当前observer属性的dep数组中。
  3. 修改Observer属性时会触发该属性的set函数。调用set时,会遍历当前Ovserver的dep数组中的watcher,调用dep.notify()通知watcher修改vdom对象,修改完成之后更新模板。

虚拟dom

虚拟dom对应的就是真实dom。虚拟dom是真实dom的抽象化,是对象。

当我们页面中dom数比较多的时候,频繁的修改、增加dom的数量,对性能会有极大的浪费。虚拟dom就是为了解决这个问题而生,它建立在真实的dom之上。当数据驱动dom修改时,它会通过diff计算,来尽量少的创建新元素,而尽可能多地复用旧的om,这样就可以减少频繁创建新dom带来的消耗。