分支切换与cleanup(二)
上一篇文章写到了,如果我我们修改了obj.text = “hello vue3”,这仍然会导致副作用函数重新执行,即使document.body.innerText的值不需要变化。
其实实际上只需要在副作用执行时,从所有与之有关的依赖集合中删除,副作用执行完毕之后,重新建立连接。这样在新的联系中,不会包含遗留的副作用函数。
由此我们需要重新设计新的副作用函数,我们需要在effect内部定义一个新的efffectFn,为其添加了一个effectFn.deps属性,用来存储当前副作用函数的依赖集合:
// 用一个全局变量存储被注册的副作用函数
let activeEffect
function effect(fn) {
const effectFn = () => {
// 当effectFn执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn
fn()
}
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
那么现在我们要关注的就是effectFn.deps数组中的依赖集合是如何收集的。
function track(target, key) {
// 没有activeEffect,直接return
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.get(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 把当前激活的副作用函数添加到依赖集合deps中
deps.add(activeEffect)
// deps就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到activeEffect.deps数组中
activeEffect.deps.push(deps) // 新增
}
如上所示,在tarck函数中我们将当前执行的副作用函数activeEffect添加到依赖集合deps中,说明deps就是一个当前副作用函数存在联系的依赖集合,于是我们添加到activeEffect.deps数组中,这样就完成了对依赖集合的收集。
有了这样的联系之后,我们就可以在每次副作用函数执行时,根据effectFn.deps获取所有相关联的依赖集合,从而将副作用函数从依赖集合中移除:
// 用一个全局变量存储被注册的副作用函数
let activeEffect
function effect(fn) {
const effectFn = () => {
// 调用cleanup函数完成清除工作
cleanup(effectFn) // 新增
activeEffect = effectFn
fn()
}
effectFn.deps = []
effectFn()
}
// cleanup函数的实现:
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
cleanup函数接收副作用函数作为参数,遍历副作用函数的effectFn.deps数组,该数组的每一项都是依赖集合,然后将该副作用函数从依赖集合中移除,最后重置effectFn.deps数组。
至此,我们的响应系统已经可以避免副作用函数产生遗留了。但是,此时如果你运行代码你会发现,这回导致代码无限循环,此时我们观察trigger代码:
function trigger(target, key) {
const depsMap = bucket.get(target)
if(!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(fn=>fn()) // 问题出现在这句代码
}
在trigge函数内部,我们遍历effects集合,里面存储着副作用函数。当副作用函数执行时,会调用cleanup进行清除,实际上就是从effects集合中将当前执行的副作用函数剔除,但是副作用函数的执行会导致其重新被收集到集合中,而此时对于effects集合的遍历仍在进行。
const set = new Set([1])
set.forEach(item => {
set.delete(1)
set.add(1)
console.log('遍历中');
})
这段代码中,我们创建了一个集合set,它里面有一个元素数字1,接着我们调用forEach遍历该集合。在遍历过程中,首先delete(1)删除数字1,然后再加入1,这段代码就会无限执行下去。
在语言规范中对此有明确的说明:在调用forEach遍历set集合时,如果一个值已经被访问过,该值就会被删除并重新添加到集合,如果此时forEach遍历没有结束,那么该值就会被重新访问。
为了解决这个问题,我们需要构造另一个Set集合:
const set = new Set([1])
const newSet = new Set(set)
newSet.forEach(item => {
set.delete(1)
set.add(1)
console.log('遍历中');
})
// 这样就不会无限执行了,回到trigger函数中,我们需要用同样的手段去避免无限执行
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()) // 新增
// effects && effects.forEach(effectFn=>effectFn()) // 删除
}
以上,我们就构造了新的effectFnToRun集合并遍历它,代替直接遍历effects集合。从而避免了无限执行。