分支切换与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(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.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); 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 obj.text = "1秒后改变数据"; }, 1000);
|
执行流程
默认第一次执行effect函数后
1 2
| 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) { const effectFn = () => { cleanup(effectFn) activeEffect = effectFn fn() } effectFn.deps = [] effectFn() }
|
再次更新时, 首先更新obj.flag的值
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
| 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.push(deps) return target[key]; }
|
此时bucket桶及deps结构为

继续更新数据
访问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) effectsToRun.forEach((fn) => fn()); return true; }
|
到此, 代码运行结束. 副作用函数只会被调用一次, 完成需求.
总结
用activeEffect.deps来临时存放所有的副作用函数集合. 通过每次调用setter函数时, 遍历deps依次清空集合来达到清空bucket桶中集合的目的. 再通过重新执行副作用函数, 搜集新的副作用函数集合, 供下次给其他属性赋值时使用, 如果该值未进入代码执行阶段, 就不会绑定副作用函数. 也就不会触发对应的副作用函数了.