vue3响应式原理-嵌套effect

501 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

响应式原理-嵌套effect

上一篇中,我们实现了 vue响应式原理-分支切换的功能

1.业务场景

我们在平时的业务开发过程中,不可避免的会使用到嵌套组件,比如:

//A组件
const A={
    render(){
        ...
    }
}
//B组件
const B={
    render(){
        return <A/>
    }
}

此时,effect就发生了嵌套,如下:

effect(()=>{
    //关于B组件数据的读取
    effect(()=>{
        //关于B组件数据的读取
        ...
    })
    ..

})

上面这样写,似乎不太清晰,我们可以用一个例子来看一下

2.例子

let obj={
    flag:true,
    count:1,
    name:"zhangsan"
}
let activeEffect=null
let effect=(fn)=>{
    //清空effectFn 的deps
    const effectFn=()=>{
        cleanUp(effectFn)
        activeEffect=effectFn
        fn()
       
    }
    effectFn.deps=[]
    effectFn()
}

let bucket=new WeakMap()
let newObj=new Proxy(obj,{
    get(target,key){
        track(target,key)
        return Reflect.get(target,key)
    },
    set(target,key,value){
        Reflect.set(target,key,value)
        trigger(target,key)
        return true
    },
})
//分支切换
function cleanUp(effectFn){
    for (let i = 0; i < effectFn.deps.length; i++) {
       const deps=effectFn.deps[i]
       deps.delete(effectFn)   
    }
    effectFn.deps.length=0
}

function track(target,key){
    let depMaps=bucket.get(target)
    if(!depMaps){
        bucket.set(target,(depMaps=new Map()))
    }
    let deps=depMaps.get(key)
    if(!deps){
        depMaps.set(key,(deps=new Set()))
    }
    deps.add(activeEffect)
//    console.log(key);
    activeEffect.deps.push(deps)
    
}

function trigger(target,key){
    let depMaps=bucket.get(target)
    let deps=depMaps.get(key)
    let effectTorun=new Set(deps)
    effectTorun.forEach(fn => {
        fn()   
    });

}


//这里做了嵌套
effect(()=>{
   
    effect(()=>{
        document.getElementById("test").textContent=newObj.count
        console.log("执行第二个函数");
    }) 
    console.log("执行第一个函数");
    document.getElementById("app").textContent=newObj.name
   
   
})

如上:我们对effect做了一层嵌套,这里在执行effect函数的时候,会收集count和name属性的依赖 我们可以打印一下,bucket

image.png 我们的页面上,id为app的容器显示的文本内容是zhangsan,id为test的容器显示的文本内容是1, 如图

image.png

3.问题

接下来,我们去修改name的值,去触发依赖

...
newObj.name="lisi"

我们接着去看页面

image.png 咦,奇怪,为什么打印了执行第二个函数,我明明触发的name所收集的依赖,而且就算执行了里面嵌套effect,那我的在下面的赋值的语句为什么没有执行? 那如果我们修改count的值呢?结果显示没有问题,页面也更新为100,同时也执行了内部嵌套的effect

...
//newObj.name="lisi"
newObj.count=100

image.png 为什么修改name的值,会出现这个问题呢

4.分析

首先,收集依赖的时候是正常收集的

当我们修改name的值,去触发依赖,我们会执行这段代码,这段代码里的activeEffect 会被track收集,所以本质上,触发的依赖函数就是activeEffect

let activeEffect=null
let effect=(fn)=>{
   //清空effectFn 的deps
   const effectFn=()=>{
       cleanUp(effectFn)
       activeEffect=effectFn
       fn()//执行fn
      
   }
   effectFn.deps=[]
   effectFn()
}

fn是什么? 就是

effect(
//fn
()=>{
    effect(()=>{
        document.getElementById("test").textContent=newObj.count
        console.log("执行第二个函数");
    }) 
    console.log("执行第一个函数");
    document.getElementById("app").textContent=newObj.name
     
}
)

我们执行的时候,发现里面还有一个effect,于是我们将内部的effect,再次赋值给activeEffect,所以当我们去修改name的值的时候,activeEffect执行的时候,只会再次读取count的值,根本不能更新name的值!

5.解决方案

我们可以跟着上面的逻辑走,当我们执行到内部嵌套的函数的时候,我们如何才能再去往下执行呢?

正常的逻辑是:我们修改了name的值,会触发count收集的依赖,然后我们再去触发name的依赖。

我们可以在effect函数内部做一些“手脚” 我们在外面定义一个effectStack=[ ],当我们修改name的时候,effectStack[effect1()] (ps:此时是外层的依赖),然后执行到内部的effect,我们再将effect push进去 effectStack[ effect1() , effect2()]

当我们触发依赖的时候,先触发栈顶的effect,执行完成之后,删除掉它,同时把栈顶的函数,再次赋值给activeEffect

具体实现逻辑如下

let effectStack=[]//effect栈
let effect=(fn)=>{
    //清空effectFn 的deps
    const effectFn=()=>{
        cleanUp(effectFn)
        activeEffect=effectFn
        effectStack.push(effectFn)
        fn()
        effectStack.pop()//执行完成之后,删除栈顶函数
        activeEffect=effectStack[effectStack.length-1] //将栈顶函数再次赋值
    }
    effectFn.deps=[]
    effectFn()
}

再次修改newObj.name="lisi",页面也就可以看到效果了

image.png