前言
在上篇文章中,我们提到了分支切换与cleanup。意思是指在代码分支发生变化时,我们需要对遗留的副作用函数进行清除(cleanup)操作,避免已经失效的副作用函数在后续过程中执行。
effect 嵌套
effect 是可以嵌套的,例如:
effect(function effectFn1() {
effect(function effectFn2(){/* ... */})
/* ... */
})
在上面这段代码中,effectFn1 就嵌套了effectFn2,effectFn1 的执行会导致effectFn2 的执行。那么什么情况下会出现嵌套的effect 呢?在Vue 中,渲染函数就是在effect 函数中执行的:
// Foo 组件
const Foo = {
render() {
return /* ... */
}
}
effect(() => {
Foo.render()
})
当组件发生嵌套的时候,例如Foo 组件中嵌套了Bar 组件:
const Bar = {
render() {
/* ... */
},
}
const Foo = {
render() {
return <Bar />
},
}
其代码可等效为:
effect(() => {
Foo.render()
effect(() => {
Bar.render()
})
})
不支持嵌套的effect 函数
上面例子说明了为什么effect 要设计成可嵌套的,接下来需要搞清楚,如果effect 不能嵌套将会发生什么?实际上,我们之前所完成的effect 并不支持嵌套,我们可以用下面的代码来测试一下:
const data = { foo: true, bar: true }
const obj = new Proxy(data, { /* ... */})
let temp1, temp2
effect(function effectFn1() {
// effectFn1 中嵌套了 effectFn2
console.log('effectFn1 执行')
effect(function effectFn1() {
console.log('effectFn2 执行')
// 在 effectFn2 中访问 obj.bar
temp2 = obj.bar
})
// 在 effectFn1 中访问 obj.foo
temp1 = obj.foo
})
obj.foo = false
在上面代码中
- 副作用函数effectFn1 嵌套了effectFn2
- effectFn1 的执行会导致effectFn2 的执行
- effectFn2 的执行在obj.foo 之前
在理想状态下,我们希望得到的依赖是:
obj
└── foo
└── effectFn1
└── bar
└── effectFn2
在这种情况下我们希望,当修改obj.foo 时,会先触发effectFn1 的执行,并间接触发effectFn2 的执行。当修改obj.bar 时,仅触发effectFn2 的执行。可实际执行结果并不是这样,我们修改obj.foo 发现结果如下
一共执行了三次,前两次输出符合预期,是两个副作用函数先后执行的结果,当修改obj.foo 时,并没有执行effectFn1 而是执行了effectFn2 ,这显然不符合我们的预期。
我们来看看这究竟是怎么回事,其实问题出在我们实现的effect 函数与acvtieEffect 变量上:
// 用全局变量存储当前激活的effect 函数
let activeEffect
function effect(fn) {
function effectFn() {
cleanup(effectFn)
// 当调用effect 注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
fn()
}
// activeEffect.deps 用来存储所以与该副作用函数相关的依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
使用effectStack 栈实现effect 嵌套
我们使用全局变量activeEffect 来存储effect 注册的副作用函数,这意味着同一时刻activeEffect 只能存储一个副作用函数。而在effect 函数发生嵌套的情况下,内部的副作用函数会将外部的副作用函数覆盖掉,且无法恢复,如果这时外部副作用函数中有响应式数据在收集依赖,那么收集到的副作用函数也是内层的副作用函数,这就是问题所在。
为了解决这个问题,我们引入一个储存副作用函数的栈effectStack 。在副作用函数执行时,将当前副作用函数压入栈中,等待副作用函数执行完毕将其从栈中弹出,并始终让activeEffect 指向栈顶元素。这样能做到一个响应式数据只会收集直接读取其值的副作用函数,不会出现相互影响的情况,代码如下所示:
let activeEffect
const effectStack = []
function effect(fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
// 执行副作用函数前,将其推入 effectStack
effectStack.push(effectFn)
// 发生嵌套时,effect 函数执行在fn 中
fn()
// 在当前副作用函数执行完毕后,将当前函数弹出,并将activeEffect 还原为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
effectFn()
}
在上面代码中,我们定义了effectStack 数组用户模拟栈,activeEffect 没有变化,它仍然指向当前正在执行的副作用函数。不同的是,当前副作用函数会被压入栈顶,当副作用函数发生嵌套时,栈底的存储的就是外部的副作用函数,栈顶储存的则是内部副作用函数。当内层副作用函数effectFn2 执行完毕后,它会从栈中弹出,并让activeEffect 设置外部副作用函数。如此一来,响应式数据只会收集读取其值的副作用函数,不会发生错乱。
总结
在这篇文章中,我们先是了解了effect 函数发生嵌套的情况,并且揭露了之前实现effect 函数的不足 —— 它并不支持嵌套。随后,我们使用了effectStack 栈来使响应式数据能够正常地收集读取其数据的副作用函数,从而使我们的effect 函数支持嵌套。