你能实现一个没有漏洞的双向绑定吗

简单的双向绑定

以下是利用 Object.defineProperty 实现的一个简单双向绑定:

1
2
3
<p>请输入:</p>
<input type="text" id="input">
<p id="p"></p>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let obj = {}
let input = document.getElementById('input')
let p = document.getElementById('p')
// 数据劫持
Object.defineProperty(obj, 'text', {
configurable: true,
enumerable: true,
get() {
console.log('获取数据了')
},
set(newVal) {
console.log('数据更新了')
input.value = newVal
p.innerHTML = newVal
}
})
// 输入监听
input.addEventListener('keyup', function(e) {
obj.text = e.target.value
})

利用 proxy 实现:

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
let input = document.getElementById('input')
let p = document.getElementById('p')
let obj = {
text: 111
}
const hander = {
get: function (target, key) {
console.log(`${key} 被读取`);
return target[key];
},
set: function(target, key, value) {
console.log(`${key} 被设置为 ${value}`);
target[key] = value
// 以下改变 input 的内容
// input.value = value
p.innerHTML = value
}
}
const proxy = new Proxy(obj, hander)
console.log(proxy.text)
// 输入监听
input.addEventListener('keyup', function(e) {
proxy.text = e.target.value
console.log(obj.text)
})

无漏洞版双向绑定

完整的双向绑定需要三个核心实现类

  • Observer : 它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新

  • Dep : 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个 Dep 实例(里面 subs 是 Watcher 实例数组),当数据有变更时,会通过 dep.notify() 通知各个 watcher

  • Watcher : 观察者对象, 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

1
2
3
<p>请输入:</p>
<input type="text" id="input">
<p id="p"></p>
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
const Vue = (function() {
let uid = 0;
// 用于储存订阅者并发布消息
class Dep {
constructor() {
// 设置id,用于区分新Watcher和只改变属性值后新产生的Watcher
this.id = uid++;
// 储存订阅者的数组
this.subs = [];
}
// 触发target上的Watcher中的addDep方法,参数为dep的实例本身
depend() {
Dep.target.addDep(this);
}
// 添加订阅者
addSub(sub) {
this.subs.push(sub);
}
notify() {
// 通知所有的订阅者(Watcher),触发订阅者的相应逻辑处理
this.subs.forEach(sub => sub.update());
}
}
// 为Dep类设置一个静态属性,默认为null,工作时指向当前的Watcher
Dep.target = null;
// 监听者,监听对象属性值的变化
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
// 遍历属性值并监听
walk(value) {
Object.keys(value).forEach(key => this.convert(key, value[key]));
}
// 执行监听的具体方法
convert(key, val) {
defineReactive(this.value, key, val);
}
}

function defineReactive(obj, key, val) {
const dep = new Dep();
// 给当前属性的值添加监听
let chlidOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
// 如果Dep类存在target属性,将其添加到dep实例的subs数组中
// target指向一个Watcher实例,每个Watcher都是一个订阅者
// Watcher实例在实例化过程中,会读取data中的某个属性,从而触发当前get方法
if (Dep.target) {
dep.depend();
}
return val;
},
set: newVal => {
if (val === newVal) return;
val = newVal;
// 对新值进行监听
chlidOb = observe(newVal);
// 通知所有订阅者,数值被改变了
dep.notify();
},
});
}

function observe(value) {
// 当值不存在,或者不是复杂数据类型时,不再需要继续深入监听
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
}

class Watcher {
constructor(vm, expOrFn, cb) {
this.depIds = {}; // hash储存订阅者的id,避免重复的订阅者
this.vm = vm; // 被订阅的数据一定来自于当前Vue实例
this.cb = cb; // 当数据更新时想要做的事情
this.expOrFn = expOrFn; // 被订阅的数据
this.val = this.get(); // 维护更新之前的数据
}
// 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用
update() {
this.run();
}
addDep(dep) {
// 如果在depIds的hash中没有当前的id,可以判断是新Watcher,因此可以添加到dep的数组中储存
// 此判断是避免同id的Watcher被多次储存
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this);
this.depIds[dep.id] = dep;
}
}
run() {
const val = this.get();
console.log(val);
if (val !== this.val) {
this.val = val;
this.cb.call(this.vm, val);
}
}
get() {
// 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
Dep.target = this;
const val = this.vm._data[this.expOrFn];
// 置空,用于下一个Watcher使用
Dep.target = null;
console.log(Dep.target, 2);
return val;
}
}

class Vue {
constructor(options = {}) {
// 简化了$options的处理
this.$options = options;
// 简化了对data的处理
let data = (this._data = this.$options.data);
// 将所有data最外层属性代理到Vue实例上
Object.keys(data).forEach(key => this._proxy(key));
// 监听数据
observe(data);
}
// 对外暴露调用订阅者的接口,内部主要在指令中使用订阅者
$watch(expOrFn, cb) {
new Watcher(this, expOrFn, cb);
}
_proxy(key) {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get: () => this._data[key],
set: val => {
this._data[key] = val;
},
});
}
}

return Vue;
})();

let demo = new Vue({
data: {
text: '',
},
});

const p = document.getElementById('p');
const input = document.getElementById('input');

input.addEventListener('keyup', function(e) {
demo.text = e.target.value;
});

demo.$watch('text', str => p.innerHTML = str);

Watcher 和 Dep 的关系:

watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。

依赖收集

  1. initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集

  2. initState 时,对侦听属性初始化时,触发 user watcher 依赖收集

  3. render()的过程,触发 render watcher 依赖收集

  4. re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值。

派发更新

  1. 组件中对响应的数据进行了修改,触发 setter 的逻辑

  2. 调用 dep.notify()

  3. 遍历所有的 subs(Watcher 实例),调用每一个 watcher 的 update 方法。

原理

当创建 Vue 实例时,vue 会遍历 data 选项的属性,利用 Object.defineProperty 为属性添加 getter 和 setter 对数据的读取进行劫持(getter 用来依赖收集,setter 用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。

每个组件实例会有相应的 watcher 实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有 computed watcher,user watcher 实例),之后依赖项被改动时,setter 方法会通知依赖与此 data 的 watcher 实例重新计算(派发更新),从而使它关联的组件重新渲染。

一句话总结:

vue.js 采用数据劫持结合发布-订阅模式,通过 Object.defineproperty 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发响应的监听回调