Vue响应式系统
# Vue响应式系统
一个响应系统的工作流程如下:
- 当读取操作发生时,将副作用函数收集到“桶”中;
- 当设置操作发生时,从“桶”中取出副作用函数并执行。
obj 是原始数据的代理对象,我 们分别设置了 get 和 set 拦截函数,用于拦截读取和设置操作。当读 取属性时将副作用函数 effect 添加到桶里,即 bucket.add(effect),然后返回属性值;当设置属性值时先更新原 始数据,再将副作用函数从桶里取出并重新执行,这样我们就实现了 响应式数据
// 存储副作用函数的桶
const bucket = new Set();
const data = { text: 'hello Vue2'}
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key){
// 将副作用函数 effect 添加到存储副作用函数的桶中
bucket.add(effect)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newVal){
// 设置属性值
target[key] = newVal
// 把副作用函数从桶里取出并执行
bucket.forEach(fn => fn())
// 返回 true 代表设置操作成功
return true
}
})
function effect() {
document.body.innerText = obj.text;
}
effect()
setTimeout(() => {
obj.text = 'hello Vue3'
}, 1000);
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
但是还存在很多缺陷,例如直接通过effect来获取副作用函数,这种硬编码的方式很不灵活。副作用 函数的名字可以任意取,我们完全可以把副作用函数命名为 myEffect,甚至是一个匿名函数,因此我们要想办法去掉这种硬编码 的机制
const bucket = new Set();
const data = { text: 'hello Vue2'}
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// 重新定义了effect函数,它变成了一个用来注册副作用函数的函数
function effect(fn){
// 当调用effect注册副作用函数时将副作用函数fn赋值给activeEffect
activeEffect = fn;
// 执行副作用函数
fn();
}
const obj = new Proxy(data, {
get(target, key){
if(activeEffect){
bucket.add(activeEffect)
}
return target[key];
},
set(target, key, newVal){
target[key] = newVal
bucket.forEach(fn => fn())
return true
}
})
effect(
() => {
console.log("effect");
document.body.innerText = obj.text
}
);
setTimeout(() => {
obj.no = "hello";
// obj.text = 'hello Vue3'
}, 1000);
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
改进后仍然存在问题:没有在副作用函数与被 操作的目标字段之间建立明确的联系。例如当读取属性时,无论读取 的是哪一个属性,其实都一样,都会把副作用函数收集到“桶”里;当 设置属性时,无论设置的是哪一个属性,也都会把“桶”里的副作用函 数取出并执行
改进桶的数据结构,使用 WeakMap 代替 Set 作为桶的数据结构:
WeakMap 由 target --> Map 构成;
Map 由 key --> Set 构成。
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// 存储副作用函数的桶
const bucket = new WeakMap();
const obj = new Proxy(data, {
get(target, key) {
if (!activeEffect) return target[key]
// 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key -->effects
let depsMap = bucket.get(target);
// 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
if (!depsMap) {
bucket.set(target,(depsMap = new Map()))
}
// 再根据key从depsMap中取得deps,它是一个 Set 类型,
// 里面存储着所有与当前key相关联的副作用函数:effects
let deps = depsMap.get(key);
// 如果 deps 不存在,同样新建一个 Set 并与 key 关联
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 最后将当前激活的副作用函数添加到“桶”里
deps.add(activeEffect);
return target[key];
},
set(target, key, newVal){
// 设置属性值
target[key] = newVal
//根据target从桶中取得depsMap它是key-->effects
const depsMap = bucket.get(target)
if (!depsMap) return
// 根据 key 取得所有副作用函数 effects
const effects = depsMap.get(key)
// 执行副作用函数
effects && effects.foreach(fn => fn());
}
})
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
其中 WeakMap 的键是原始对象 target,WeakMap 的值是一个 Map 实例,而 Map 的键是原始对象 target 的 key,Map 的值是一个 由副作用函数组成的 Set
为什么使用WeakMap?
WeakMap 对 key 是弱引用,不影响垃圾回收器的工 作。所以 WeakMap 经常用于存储那些只有当 key 所引 用的对象存在时(没有被回收)才有价值的信息,例如上面的场景 中,如果 target 对象没有任何引用了,说明用户不再需要它了, 这时垃圾回收器会完成回收任务。但如果使用 Map 来代替 WeakMap, 那么即使用户的代码对 target 没有任何引用,这个 target 也不 会被回收,最终可能导致内存溢出。
最后简单封装
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
const obj = new Proxy(data, {
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
// 把副作用函数从桶里取出并执行
trigger(target, key)
}
});
// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target,(depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.foreach(fn=fn());
}
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
# 分支切换与 cleanup
在 effectFn 函数内部存在一个三元表达式,根据字段 obj.ok 值的不同会执行不同的代码分支。当字段 obj.ok 的值发生变化时, 代码执行的分支会跟着变化,这就是所谓的分支切换
const data = { ok: true, text: 'hello' }
const obj = new Proxy(data,{})
effect(function effectFn() {
document.body.innerText = obj.ok ? obj.text : 'not'
})
2
3
4
5
分支切换可能会产生遗留的副作用函数。拿上面这段代码来说, 字段 obj.ok 的初始值为 true,这时会读取字段 obj.text 的值, 所以当 effectFn 函数执行时会触发字段 obj.ok 和字段 obj.text 这两个属性的读取操作