Vue3的响应式API是怎么实现的?(effect特别篇②)

133 阅读4分钟

上一篇文章介绍了effect中的cleanup,这篇文章介绍一下effect的嵌套执行问题,Vue对effect的嵌套执行的处理也是一个非常经典的处理办法:模拟栈结构。这个处理方式在Vue中使用了多次,比如我的第一篇文章中block的收集就使用了这种方式来处理递归过程中的dynamicChildren。

嵌套的effect:

来看一段代码:

const data = {
  foo: 1,
  bar: 1
}

const obj = reactive(data)

let tmp1, tmp12

effect(() => {
  console.log('effect1执行')
  effect(() => {
    console.log('effect2执行')
    tmp2 = obj.bar
  })

  tmp1 = obj.foo
})

// 会打印什么呢?
obj.foo++

我们把外层的effect叫做effect1,内部的effect叫做effect2。

在这段代码中,effect1执行触发了effect2执行,在effect1中读取了foo,在effect2中读取了bar,所以理想情况下,我们想要的是effect1和obj.foo建立关联,effect2和obj.bar建立关联。

但是真的是这样吗?我们不妨来分析一下。effect1的运行会注册副作用函数,也就是我们之前文章中提到的effectFn,我们给他起个名字effectFn1,这时候会运行effectFn1(本例代码没有lazy),把全局的activateEffect注册为当前的effectFn1。

effectFn,是传递给effect回调的包装函数,也就意味着运行它会运行这个回调,运行回调就进入了回调的函数体然后运行effect2,运行effect2的时候又会注册副作用函数,然后把全局的activateEffect注册为effect中的effectFn,我们给他起个名字effectFn2。然后进入effect2的回调,读取响应式数据obj.bar,这就是我们所熟悉的响应式工作原理,我就不再赘述了。

执行完成后跳出effect2,这时候会执行读取obj.foo,这时候就要注意了,obj.bar读取的时候,会收集activateEffect,但是这时候的activateEffect被注册为effectFn2了,所以导致obj.bar的deps会收集effectFn2。

所以我们在这段代码执行完成后,会发现打印出的是:'effect1执行','effect2执行','effect2执行',其中前两次执行时effect函数运行时打印的,第三次打印时修改obj.foo的值时,打印出的并非我们理想中的'effect1执行',而是'effect2执行'。

这里的问题就处在effect嵌套导致全局的activateEffect会被内部的effect函数注册的副作用函数所取代,为了解决这个问题,Vue使用了一个栈结构来保存嵌套的当前上下文的activateEffect,执行完内部effect后就弹出当前的activateEffect然后指针左移,把父级上下文的activateEffect重新拿出来,从而实现正确的effect嵌套执行。

这里我们把上一节中的effect哈函数搬过来继续完善:

let activateEffect
let effectStack = []

function effect(fn, options = {}) {
  const effectFn = () => {
    //运行伊始,执行清理过程
    cleanup(effectFn)

    activateEffect = effectFn

    // 保存当前上下文的activateEffect
    effectStack.push(activateEffect)
    const res = fn()
    // activateEffect收集结束,弹出当前activateEffect
    effectStack.pop()
    // activateEffect指向父级上下文中的activateEffect
    activateEffect = effectStack[effectStack.length - 1]

    return res
  }
  // 将options添加到effectFn上
  effectFn.options = options
  // 保存所有与该副作用函数相关的依赖
  effectFn.deps = []
  // 如果option中不指定懒执行
  if (!options.lazy) {
    effectFn()
  }
  return effectFn
}

可以看到Vue给出的解决方案,每次在effect函数注册了activateEffect后,把他保存在effectStack中,这样,当effect嵌套执行的时候,栈顶的永远保存当前上下文中的activateEffect,执行完收集过程后,销毁栈顶的activateEffect,然后指针往左移,指向父级上下文的activateEffect,这样就完美解决了我们例子中的effect嵌套导致副作用函数收集出错的问题,不得不感叹Vue设计之巧妙。

好了,到这里effect函数的核心逻辑和边界处理就介绍的差不多了,除了这两篇所介绍的问题外,还有effect函数的调度执行的问题,在computed那篇文章中有介绍,相信你如果理解了响应式原理,那么scheduler也很简单,就是不执行收集的副作用函数了,转而执行scheduler。effect还有一个问题就是无限递归问题,在 ref篇上也有讲到,就不赘述了。

下面会介绍一下reactive对于对象和数组的实现,因为我们之前都是以对象为例进行介绍,所以重点会放在数组身上。