effect是可以嵌套的,比如:
effect(function effectFn1(){
effect(function effectFn2(){
/* ... */
})
/* ... */
})
上面代码中,effectFn1的执行会导致effectFn2的执行。
那么在什么场景下会出现嵌套的effect?
在Vue.js中,当组件发生嵌套的时候,例如Foo组件渲染Bar组件时,就发生了effect嵌套,因为渲染函数就是在一个effect中执行的,看下面代码:
//Foo组件
const Foo = {
render(){
return /* ... */
}
}
相当于
effect(()=>{
Foo.render()
})
而当Foo组件渲染Bar组件
//Bar组件
const Bar = {
render(){
return /* ... */
}
}
// Foo组件渲染了Bar组件
const Foo= {
render(){
return <Bar /> // jsx语法
}
}
此时就发生了effect嵌套,其相当于:
effect(()=>{
Foo.render()
//嵌套
effect(()=>{
Bar.render()
})
})
接下来要搞清楚,effect不支持嵌套会发生什么。实际上,我们前面实现的响应式系统并不支持effect嵌套,可以用下面的代码测试下:
// 原始数据
const data = {foo:true, bar:true}
//代理对象
const obj = new Proxy(data, {/* ... */})
// 全局变量
let temp1, temp2
// effectFn1 嵌套 effectFn2
effect(function effectFn1(){
console.log('effectFn1 执行')
effect(function effectFn2(){
console.log('effectFn2 执行')
// 在effectFn2 中读取obj.bar属性
temp2 = obj.bar
})
// 在effectFn1 中读取obj.foo属性
temp1 = obj.foo
})
在理想情况下,我们希望副作用函数与对象属性之间的联系如下:
data
- foo
- effectFn1
- bar
- effectFn2
在这种情况下,我们希望修改data.foo时会触发effectFn1执行。但由于effectFn2嵌套在effectFn1里,所以会简介触发effectFn2执行,而当修改obj.bar时,只会触发effectFn2执行。但是当我们尝试修改obj.foo的值,会发现输出
'effectFn1 执行'
'effectFn2 执行'
'effectFn2 执行'
可以发现修改了字段obj.foo的值,发现effectFn1并没有重新执行,反而使得effectFn2重新执行了,这显然不符合预期。
问题出在哪里?其实就出在实现的effect函数与activeEffect上,观察下面这段代码:
// 用一个全局变量存储当前激活的effect函数
let activeEffect
function effect(fn){
const effectFn = () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数复制给activeEffect
activeEffect = effectFn
fn()
}
// activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
这里用全局变量activeEffect来存储通过effect函数注册的副作用函数,也就是说同一时刻activeEffect所存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖activeEffect的值,并且永远不会恢复到原来的值。
这时如果有响应式数据进行依赖收集,即使这个响应式数据是在外层副作用函数中读取的,它们收集到的副作用函数也都会是内层副作用函数,这就是问题所在。
为了解决这个问题,需要一个副作用函数栈effectStack,在副作用函数执行时,将当前副作用函数压入栈中,待副作用函数执行完后将其从栈中弹出,并始终让activeEffect指向栈顶的副作用函数。这样就做到一个响应式数据只会收集直接读取其值的副作用函数,而不会出现互相影响的情况。代码如下:
let activeEffect
// effect 栈
const effectStack = []
function effect(fn){
const effectFn = () =>{
cleanup(effectFn)
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn) // 新增
fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把activeEffect还原为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
effectFn()
}
需要注意的是当内层副作用函数effectFn执行完毕后,它会被弹出栈,并将副作用函数effectFn1设置为activeEffect。
如图:
这样,响应式数据就只会收集直接读取其值的副作用函数作为依赖,从而避免发生错乱。