4.4 分支切换与 cleanup
分支切换后会留下「遗留的副作用函数」导致多余更新;用
cleanup每次执行前清空旧依赖,再用一份遍历副本避开由此引发的无限循环。
一、什么是分支切换
const data = { ok: true, text: 'hello world' }
const obj = new Proxy(data, { /* track / trigger ... */ })
effect(function effectFn() {
document.body.innerText = obj.ok ? obj.text : 'not'
})
obj.ok为true时,effectFn读取了obj.ok与obj.text,两个字段的依赖集合里都有它;- 把
obj.ok改成false后只走'not'分支,不再读取obj.text; - 但
obj.text的依赖集合里仍然留着effectFn→ 此时修改obj.text,effectFn依旧会执行(多打印一次),即使页面内容根本不需要变化。
现象:遗留的副作用函数会导致不必要的更新;分支切换越频繁,遗留越多。
二、解决思路
每次副作用函数执行前,先把它从所有与之关联的依赖集合中删除;执行过程中读到哪些字段会重新收集它,于是新的联系里不再包含被淘汰的分支。
三、重新设计副作用函数
function effect(fn) {
const effectFn = () => {
cleanup(effectFn) // 执行前先清除旧依赖
activeEffect = effectFn
fn()
}
// effectFn.deps:存放「包含该副作用函数的所有依赖集合」
effectFn.deps = []
effectFn()
}
function cleanup(effectFn) {
// 遍历 effectFn.deps,把自己从每个依赖集合中删掉
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
// 重置数组,等待本次执行重新收集
effectFn.deps.length = 0
}
配套地,在 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)
// deps 就是与当前副作用函数有联系的依赖集合,记下来供 cleanup 使用
activeEffect.deps.push(deps)
}
track里必须执行activeEffect.deps.push(deps),否则cleanup根本不知道要去哪儿删除自己。
四、新问题:无限循环
只加上 cleanup 后会死循环。原因是 trigger 直接遍历 effects,而副作用函数执行过程中又把自己从同一个 Set 中删除再添加回来:
const set = new Set([1])
set.forEach(item => {
set.delete(1)
set.add(1)
console.log('遍历中') // 永远不会结束
})
语言规范对此有明确规定:用
forEach遍历Set时,若某个值已被访问过、随后被删除又重新添加,且此时遍历尚未结束,那么该值会被重新访问。cleanup删除 +track重新添加,正好命中这条规则。
解决办法:构造另一个 Set 再遍历:
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
// 复制一份再遍历,避免遍历中增删集合导致的无限执行
const effectsToRun = new Set(effects)
effectsToRun.forEach(effectFn => effectFn())
}
五、易错点
cleanup必须在fn()之前调用:先清空、再由本次读取重新建立联系,这才是「没有遗留」的保证。 一旦引入cleanup,trigger就不能再直接forEach(effects),必须遍历副本。 实测:obj.ok = false会触发 2 次(首次执行 + 变更各一次),之后修改obj.text不再触发。
相关
- 4.3 设计一个完善的响应系统:三层依赖结构与 track / trigger
- 4.5 嵌套的 effect 与 effect 栈:全局
activeEffect带来的下一个问题