三、vue响应式原理:处理因分支切换造成的不必要的依赖收集

286 阅读2分钟

这篇文章我们来处理一个问题:分支切换可能会造成不必要的依赖被收集。

这里的分支切换指的是三元运算符,而不是git中的概念,我们在代码中时常用到三元表达式,比如:

// 原始数据
const data = {
  flag: true,
  text: 'hello world'
}
// 对原始数据的代理
const dataProxy = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    trigger(target, key)
  }
})

function render() {
  console.log('num')
  return dataProxy.flag ? dataProxy.text : 'other message'
}
effect(render)

上面代码我们在render中使用了三元运算符,可以看到flag和text属性都收集到了render的依赖函数,当我们改变这两个属性的值时,render函数将会被再次执行。

这看上去并没有什么问题,但是当我们将flag赋值为false的时候,render函数将永远不会执行到dataProxy.text,也就是说此时的render函数不应该被text属性收集,那么我们改如何处理这个问题呢?

其实很简单,在我们每次执行render之前,将上次收集到的所有render依赖移除掉,然后执行render的时候再收集相应的依赖,这样就能保证不会有多余的依赖,具体实现:

function effect(fn) {
  // 将fn的执行使用effectFn嵌套一层,方便后续操作
  const effectFn = () => {
    // 清除所有被收集的当前副作用函
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
    activeEffect = effectFn
    fn()
  }
  // 存储所有收集effectFn的集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()
}
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    effectFn.deps[i].delete(effectFn)
  }
  // 清空deps数组
  effectFn.deps.length = 0
}
function track(target, key) {
  // 省略部分代码
  // 将当前集合收集到副作用函数中的deps数组里 方便清除
  activeEffect.deps.push(deps)
}

以上,我们定义一个effectFn函数对fn进行包裹,然后给effectFn一个deps属性用来收集所有收集过effectFn的依赖集合,以便在每次执行fn前的清除工作,而deps数组的填充也非常简单,我们只需要在track时将当前key对应的依赖集合push到deps中即可。

如果此时你尝试运行代码会发现会进入到死循环,原因很简单,这是因为集合遍历的一个特性,如果我们在遍历过程中对集合进行了增删操作,那么forEach就会再次执行,如下面代码

const set = new Set([1])
// 解决办法就是重新定义一个set 对新set进行遍历即可
// const newSet = new Set(set)
set.forEach(item => {
  set.delete(1)
  set.add(1)
  console.log(999)
})

其实我们的trigger函数中也有类似操作,我们在对effects进行遍历的时候用于会执行cleanup然后重新收集依赖,所以就会出现无限循环的情况,而解决方法也很简单,只需要定义一个新的set

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  // 定义一个新的set 遍历新set即可
  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => effectsToRun.add(effectFn))
  effectsToRun.forEach(effectFn => effectFn())
}

这样我们就解决了因为分支切换造成的不必要的依赖被收集的问题