上一篇文章介绍了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对于对象和数组的实现,因为我们之前都是以对象为例进行介绍,所以重点会放在数组身上。