【vue设计与实现】响应系统的作用与实现 3-嵌套effect和effect栈

本文探讨了Vue.js中effect函数的嵌套问题及其在组件渲染时的影响。当组件嵌套导致effect函数嵌套执行时,原本的响应式系统可能出现预期外的行为。分析了错误的原因在于全局activeEffect只存储单个副作用函数,导致依赖收集混乱。为解决此问题,提出了使用副作用函数栈effectStack的解决方案,确保每个响应式数据只收集直接读取它的副作用函数,保持依赖关系的正确性。
摘要由CSDN通过智能技术生成

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。
如图:
请添加图片描述
这样,响应式数据就只会收集直接读取其值的副作用函数作为依赖,从而避免发生错乱。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值