解析vue双向绑定原理

响应式数据/双向绑定原理

vue 的响应式数据/双向绑定主要是指:数据变化更新视图,视图变化更新数据。 将 data 中的 js 对象做遍历,利用 Object.defineProperty 将 data 中的数据全部转换成 getter/setter,每个组件都对应一个 Watcher 实例,收集数据中的依赖,当数据发生改变触发 setter 时,会通知对应的组件,组件通过虚拟 DOMdiff 算法再做改变,如官网的流程图所示:

简单的双向绑定代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<div id="app">
<input type="text" id="txt" />
<p id="show"></p>
</div>
</body>
<script type="text/javascript">
var obj = {};
Object.defineProperty(obj, "txt", {
get: function () {
return obj;
},
set: function (newValue) {
document.getElementById("txt").value = newValue;
document.getElementById("show").innerHTML = newValue;
}
});
document.addEventListener("keyup", function (e) {
obj.txt = e.target.value;
});
</script>

监听器 observer

对 data 里面的数据对象做遍历(子对象存在时做递归遍历),利用 Object.defineProperty 将 data 中的数据全部转换成 getter/setter,当某个属性和值发生改变时就能触发 setter,就会监听到数据的变化

模板解析器 Compile

模板解析器主要通过遍历模板,查看都使用了哪些变量、指令,为数据添加订阅者 Watcher,一旦数据发生了改变,调用更新函数更新。

订阅者 Watcher

Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新——这是一个典型的观察者模式

订阅器 Dep

订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

实现一个通用的发布订阅模式

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
// 定义事件中心类
class MyEvent {
handlers = {}; // 存放事件 map,发布者,存放订阅者

$on(type, fn) {
if (!Reflect.has(this.handlers, type)) {
// 如果没有定义过该事件,初始化该订阅者列表
this.handlers[type] = [];
}
this.handlers[type].push(fn); // 存放订阅的消息
}

$emit(type, ...params) {
if (!Reflect.has(this.handlers, type)) {
// 如果没有该事件,抛出错误
throw new Error(`未注册该事件${type}`);
}
this.handlers[type].forEach(fn => {
// 循环事件列表,执行每一个事件,相当于向订阅者发送消息
fn(...params);
});
}

$remove(type, fn) {
if (!Reflect.has(this.handlers, type)) {
throw new Error(`无效事件${type}`);
}
if (!fn) {
// 如果没有传入方法,表示需要将该该类型的所有消息取消订阅
return Reflect.deleteProperty(this.handlers, type);
} else {
const inx = this.handlers[type].findIndex(handler => handler === fn);
if (inx === -1) {
// 如果该事件不在事件列表中,则抛出错误
throw new Error("无效事件");
}
this.handlers[type].splice(inx, 1); // 从事件列表中删除该事件
if (!this.handlers[type].length) {
// 如果该类事件列表中没有事件了,则删除该类事件
return Reflect.deleteProperty(this.handlers, type);
}
}
}
}