Vue3设计与实现共读-响应系统(五)

270 阅读3分钟

分支切换与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集合。从而避免了无限执行。