vue3响应系统(二)

分支切换与cleanup

在vue3响应系统(一)中实现了最简单的数据双向绑定. 但是当出现分支切换的情况时, 会存在触发多余的副作用函数的情况.

什么是分支切换

当副作用函数中存在条件判断时, 如下所示.

1
2
3
4
5
6
7
8
9
// 原始数据
const data = { text: "hello world", flag: true };
// 代理
const obj = new Proxy(data, {get(){/*...*/}, set(){/*...*/}})
// 副作用函数
effect(() => {
console.log('effect -- trigger')
document.body.innerHTML = obj.flag ? obj.text: 'flag';
});

初始数据flag:true时, 第一次执行effect函数, obj.flag和obj.text都会触发getter方法, 存储桶中会分别将effect绑定在flag和text下. 对应的副作用函数都会执行.

再次赋值

1
2
3
4
setTimeout(() => {
obj.flag = false
obj.text = "1秒后改变数据";
}, 1000);

当obj.flag == false时, document.body.innerHTML = obj.flag ? obj.text: 'flag'中obj.text不会被执行. 此时key: text的副作用函数不应该被调用, 也就是说副作用函数只会被调用一次. 但是实际effect会被调用两次!

effect被调用两次

解决方案

问题的根源出在, 第一次effect(effectFn)时, Proxy触发两次getter. 此时bucket为{target: {flag: [effectFn], text: [effectFn]}}, 后面也不会再更新bucket的内容.

当再次给obj.text赋值时, setter函数中会再次找到对应的effectFn再次执行.

所以我们应该在每次effect函数执行时, 先清空bucket并实时更新. 也就是说, bucket的内容应该是实时变化的.

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
const bucket = new WeakMap();

let activeEffect;

const data = { text: "hello world", flag: true };

const obj = new Proxy(data, {
get(target, key) {
if (!activeEffect) return target[key];
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);
// 将其添加到activeEffect.deps数组中
activeEffect.deps.push(deps)

return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
// 把副作用函数从桶里取出并执行
// * 因为执行过程中要执行delete操作, 遍历时会导致死循环. 所以重新new一个Set对象
const effectsToRun = new Set(effects)
effectsToRun.forEach((fn) => fn());
return true;
},
});

// 副作用函数
function effect(fn) {
const effectFn = () => {
// 每次先清除数据
cleanup(effectFn)
activeEffect = effectFn
fn()
}
effectFn.deps = []
effectFn()
}

function cleanup(effectFn) {
// 遍历数组并清空
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
deps.delete(effectFn)
}
// 重置数组
effectFn.deps.length = 0
}

effect(() => {
document.body.innerHTML = obj.flag ? obj.text: 'flag';
});

setTimeout(() => {
obj.flag = false // 第一次执行setter函数后,
obj.text = "1秒后改变数据";
}, 1000);

执行流程

默认第一次执行effect函数后

1
2
// obj.flag === true
document.body.innerHtml = obj.text

此时存储桶bucket和activeEffect.deps结构如图. 此时bucket及deps中存储的都是effectFn函数

初始存储结构

effectFn函数内容为下

1
2
3
4
5
6
7
8
9
10
11
12
function effect(fn) {
// effectFn 函数定义
const effectFn = () => {
// 每次先清除数据
cleanup(effectFn)
activeEffect = effectFn
fn()
}
effectFn.deps = []
effectFn()
}

再次更新时, 首先更新obj.flag的值

1
obj.flag = false // 触发setter函数

setter函数会遍历对应key中的Set, 随后effectFn函数被调用

1
2
3
4
5
6
7
8
9
set(target, key, newVal) {
target[key] = newVal;
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const effectsToRun = new Set(effects)
effectsToRun.forEach((fn) => fn());
return true;
},

effectFn函数第一步会找到effectFn.deps, 遍历并清空里面的内容

1
2
3
4
5
6
7
8
9
function cleanup(effectFn) {
// 遍历数组并清空
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
deps.delete(effectFn)
}
// 重置数组
effectFn.deps.length = 0
}

此时bucket桶及deps内容都为空

清空内容

再将effectFn赋值给activeEffect, 最后执行副作用函数. 执行结果为:

1
2
// obj.flag === false
document.body.innerHtml = 'flag'

期间只有obj.flag的getter函数被调用, 也就是只有’key === flag’对应的Set新增了activeEffect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
get(target, key) {
if (!activeEffect) return target[key];
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);
// 将其添加到activeEffect.deps数组中
activeEffect.deps.push(deps)
return target[key];
}

此时bucket桶及deps结构为

bucket及deps结构

继续更新数据

1
obj.text = "1秒后改变数据";

访问setter函数, text对应下的Set集合为空! 后面也不会调用副作用函数了.

1
2
3
4
5
6
7
8
9
set(target, key, newVal) {
target[key] = newVal;
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const effectsToRun = new Set(effects) // effects此时为Set(0) 空
effectsToRun.forEach((fn) => fn());
return true;
}

到此, 代码运行结束. 副作用函数只会被调用一次, 完成需求.

总结

用activeEffect.deps来临时存放所有的副作用函数集合. 通过每次调用setter函数时, 遍历deps依次清空集合来达到清空bucket桶中集合的目的. 再通过重新执行副作用函数, 搜集新的副作用函数集合, 供下次给其他属性赋值时使用, 如果该值未进入代码执行阶段, 就不会绑定副作用函数. 也就不会触发对应的副作用函数了.