四、vue响应式原理:effect嵌套与同时进行赋值取值操作死循环

608 阅读3分钟

effect嵌套

开发中我们肯定不会只收集render函数,在render函数中肯定还会想收集其他的副作用函数,如某些methods,我们也想在对应的依赖改变的时候重新执行对应的methods,这样我们在使用effect函数时就会发生嵌套,如下代码

// 原始数据
const data = { foo: true, bar: true }

let temp1, temp2
effect(function render() {
  console.log('render 执行')
  effect(function effectFn1() {
    console.log('effectFn1 执行')
    temp2 = dataProxy.bar
  })
  temp1 = dataProxy.foo
})
setTimeout(() => {
  dataProxy.foo = false
}, 1000);

执行上面代码我们会发现,一秒后 effectFn1 执行 重新打印了一遍,这种结果显然是不对的,我们一秒后改变的是foo的值,那么应该会将render函数重新执行一遍,应该把 render 执行effectFn1 执行 都再次打印一遍。

其实问题很简单,我们逐步分析一下。当执行外层的effect时,全局的activeEffect的值为render函数,在执行render函数的过程中又遇到内层的effect,所以此时代码会将activeEffect的值赋值为effectFn1函数,而当内层的effect执行完成后去执行temp1 = dataProxy.foo这一行代码我们会发现,此时全局的activeEffect值依然是effectFn1函数,所以foo就收集到effectFn1函数而不是render函数,所以后来执行foo相关依赖的时候就执行的是effectFn1函数。

相信到这里大家应该都能想到对应的解决方案,我们只需要保证全局的activeEffect的值是正确的就可以了。我们这里使用一个栈结构来存储所有层级effect对应的副作用函数,我们需要保证activeEffect永远指向栈顶的副作用函数,当栈顶的副作用函数执行完成后将其弹出即可,以下为具体实现

// effect 栈
const effectStack = []
function effect(fn) {
  const effectFn = () => {
    // 清除所有被收集的当前副作用函
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
    activeEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压栈
    effectStack.push(effectFn)
    fn()
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  // 存储所有收集effectFn的集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()
}

同时赋值取值

前面我们编写的用来测试的render副作用函数只对代理对象进行了读取操作,当我们尝试同时进行赋值取值的操作时会发现新的问题,如下render函数

function render() {
  dataProxy.flag = !dataProxy.flag
}
effect(render)

运行代码我们会发现报了一个栈溢出的错误:Maximum call stack size exceeded

原因很简单,我们执行dataProxy.flag = !dataProxy.flag时是先执行读的操作的,所以当读取到dataProxy.flag的值时,render函数已经被flag对应的集合收集到了,当我们对flag进行赋值操作的时候,render函数会被取出来再次执行,这样就进入了一个死循环。

解决方案很简单,我们在取依赖执行的时候,要判断一下取出来执行的依赖函数是不是当前全局的依赖函数,如果是就不要执行

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  effectsToRun.forEach(effectFn => effectFn())
}

以上就是两个问题的具体解释及处理,下节将会实现调度器和懒执行配置,这两个配置是实现computed和watch的关键