vue3响应式原理-分支切换

297 阅读3分钟

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

vue3响应式原理——分支切换

上次简单的实现了vue3响应式原理,track收集依赖,trigger触发依赖可戳该链接查看,这次我们来聊一聊分支切换。

不知道有没有小白和我一样懵逼,啥,分支切换?这是什么鬼,我只知道git的分支切换,莫慌,🤔 我们先看一个🌰

例子

如果我们要收集的依赖函数长这样

let obj={
    flag:true,
    count:1,
    name:"zhangsan"
}
let newObj=new Proxy(obj,{
        get(){
               ...
            },
        set(){
            ...
            }
        })
//...
//主要看这里⬇️
effect(()=>{
    console.log("执行了effect函数")
   newObj.flag? document.getElementById("app").textContent=newObj.name : "字符串11"
   
})
effect()
console.log(bucket)

effect函数执行时,会读取flag 和name的值,此时会对我们的flag和name进行依赖收集,我们在这里可以打印一下,最终bucket的值是什么

image.png

没问题,的确对flage和name的值进行依赖的收集了。 接下来,我们试着去修改flag的值

newObj.flag=false

此时flag为false,

1.document.getElementById("app") 的值会是“字符串11”,

2.同时会打印两次 “执行了effect函数”,一次是收集依赖的时候,一次是触发依赖的时候

如果我们此时再修改

newObj.name="lisi"

我们会发现又打印了一次 “执行了effect函数”

image.png 这是怎么回事?🤔️🤔️

没错,就是我们收集依赖的时候,收集到了name属性的依赖函数,所以在修改name属性值的时候,才会出发effect函数。

问题

可是,这样真的合理吗?

1.effect函数中,如果flag为false,我们只会走false的分支,我们不care newOBj.name的值是多少,我在修改name值的时候,又会触发effect函数,这样的更新是必要的吗?

往更大点说,如果我们在执行effect函数时,收集了n多个属性的依赖,每修改一个属性就去触发effect函数,😱 难以想象

简单来讲,这种收集依赖的方式会导致不必要的更新

思考

如何解决呢? 首先,什么是合理的? 比如我第一次flag为true时,会收集flag和name的依赖

当我们修改flag的值时,去触发依赖,此时,我们的effect函数应该处于一个空的状态,等待下次用户再访问属性时,做新的一次依赖收集

解决方式

即:每次执行effect函数时,我们可以先把他从从所有与之关联的依赖集合中删除

1.首先,effect函数修改如下

let effect=(fn)=>{
    const effectFn=()=>{
        activeEffect=effectFn 
        fn()
    }
   
    effectFn.deps=[] //用来存储所有与该副作用函数相依赖的集合
    effectFn()
}

2.我们在收集依赖时,将所有

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)
    //将其添加进去 [deps,deps,deps] ,这里的deps就是{flag: deps }  =>flag的依赖函数集 
    //这里的deps就是{name: deps }  =>name的依赖函数集
    activeEffect.deps.push(deps) 
    
}


3.再在effect中添加代码

let effect=(fn)=>{
    const effectFn=()=>{
        //清空掉effect函数关联的依赖集合
         for(let i=0;i<effectFn.deps.length;i++){
                      //比如:i=0 这里的deps 就是flag收集的依赖集合
                       let deps=effectFn.deps[i]
                      //我们删除掉deps里的依赖的集合,这样再去setter()修改的时候,
                       //发现deps的长度使0,就不会触发依赖了
                       deps.delete(effectFn)
                             }
                       effectFn.deps.length=0
        activeEffect=effectFn 
        fn()
    }
   
    effectFn.deps=[] //用来存储所有与该副作用函数相依赖的集合
    effectFn()
}

4.当我们这时候,去查看效果的时候,发现!!

image.png

what!这是什么情况,为啥栈溢出了?

我们思考这样一个场景

const set=new Set([1]);
set.forEach(function(){
    set.delete(1)
    set.add(1)
    console.log(999)
})

forEach在遍历set时,如果一个值已经被访问过,但该值被删除并重新添加到集合,如果此时forEach还没有结束,那么该值会被重新访问 解决这个问题很简单 我们可以在触发依赖的时候,构造一个set集合,并遍历

//触发依赖
function trigger(target,key){
    let depMaps=bucket.get(target)
    let effects=depMaps.get(key)
     const effectToRun=new set(effects)
     effectToRun.forEach(effectFn()=>{
                 effectFn()
             })
    //deps && deps.forEach(effect => {
    //   effect()   
    //});

}

完整代码如下:

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 effects=depMaps.get(key)

     let effectTorun=new Set(effects) //set保证具有唯一性
     effectTorun.forEach(effectFn => {
         effectFn()   
     });

}
effect(()=>{
    console.log("执行了effect函数")
   newObj.flag? document.getElementById("app").textContent=newObj.name : "123"
   
})
console.log(bucket)
newObj.flag=false

总结:

分支切换,本质上是在收集依赖时,把和该副作用函数有联系的属性所收集的effects,收集起来,再在执行副作用函数时清空掉,当副作用函数执行完毕后,会重新建立联系