vue.js设计与实现(四)-响应系统-嵌套的effect与ef
描述:
在vue框架中我们会遇到组件的嵌套等等,那么嵌套的实现,当前程序代码是否可以实现,如下测试:
const data = {foo:'foo',bar:'bar'}
const bucket = new WeakMap()
let activeEffect;
// 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()
}
function cleanup(effectFn){
for(let i=0;i<effectFn.deps.length;i++){
effectFn.deps[i].delete(effectFn)
}
effectFn.deps.length = 0
}
function track(target,key){
if(!activeEffect) return
let depsMap = bucket.get(target)
if(!depsMap){bucket.set(target,depsMap=new Map())}
let deps = depsMap.get(key)
if(!deps){depsMap.set(key,deps=new Set())}
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
function trigger(target,key){
let depsMap = bucket.get(target)
if(!depsMap) return
const effects=depsMap.get(key)
const effectsToRun = new Set(effects)
effectsToRun.forEach(effect=>effect())
}
const obj = new Proxy(data,{
get(target,key){
track(target,key)
return target[key]
},
set(target,key,newVal){
target[key] = newVal
trigger(target,key)
}
})
let temp1,temp2;
effect(
function effectFn1(){
console.log('effectFn1执行')
effect(function effectFn2(){
console.log('effectFn2执行')
temp2 = obj.bar
})
temp1 = obj.foo
}
)
setTimeout(()=>{
obj.foo = 'foolll'
})
我们希望当修改 obj.foo 时会触发 effectFn1 执行。由于effectFn2 嵌套在 effectFn1 里,所以会间接触发 effectFn2 执行,而当修改obj.bar 时,只会触发 effectFn2 执行。但结果不是这样的,我们尝试修改obj.foo 的值,会发现输出为:
image.png
一共打印三次,前两次分别是副作用函数 effectFn1 与 effectFn2 初始执行的打印结果,到这一步是正常的,问题出在第三行打印。我们修改了字段 obj.foo 的值,发现 effectFn1 并没有重新执行,反而使得 effectFn2 重新执行了,这显然不符合预期。
原因:
我们用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,这意味着同一时刻 activeEffect 所存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。这时如果再有响应式数据进行依赖收集,即使这个响应式数据是在外层副作用函数中读取的,它们收集到的副作用函数也都会是内层副作用函数,这就是问题所在。
为了解决这个问题,我们需要一个副作用函数栈 effectStack,在副作用函数执行时,将当前副作用函数压入栈中,待副作用函数执行完毕后将其从栈中弹出,并始终让 activeEffect 指向栈顶的副作用函数。这样就能做到一个响应式数据只会收集直接读取其值的副作用函数,而不会出现互相影响的情况,如以下代码所示:
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()
}
打印结果:
image.png如此一来,响应式数据就只会收集直接读取其值的副作用函数作为依赖,从而避免发生错乱。