vue:手动实现一个相对完善的响应式系统(二)

47 阅读5分钟

这篇单纯为本人学习《vue设计与实现》做的笔记,写得并不好,建议直接阅读这本书

继上篇 juejin.cn/post/726492…

4.分支切换与cleanup

首先我们要知道什么是分支切换,如代码所示

    const data = {
        ok:true,
        text:"hello word"
    }
    const obj = new Proxy(data,{...})
    effect(function effectFn(){
        document.body.innerText = obj.ok?obj.text:"not"
    })

这个副作用函数里是一个三元表达式,当obj.ok发生不同变化时,会执行不同的代码,这就是分支切换 拿上面的函数来说,当obj.ok为true时,会读取obj.text字段,副作用函数会被当做依赖函数所收集,这确实是我们想要的结果。但是,当我们修改obj.ok为false时,obj.text并不会被读取,但是之前的副作用函数已经被作为依赖函数收集到bucket中,我们修改obj.text值时,副作用函数依然会执行,这会造成不必要的更新,代码如下所示

    const data = {
        ok:true,
        text:"hello word"
    }
    const obj = new Proxy(data,{...})
    effect(function effectFn(){
        document.body.innerText = obj.ok?obj.text:"not"
    })
    
    obj.ok = false
    //这会触发更新,但是此时obj.textfalse,所以不在读取obj.text,我们再怎么修改obj.text 应该是不能再触发更新,但是结果并不是这样
    obj.text = "hello vue3" //这时会触发更新
    

原因正如上面所说,当obj.ok为true时,已经读取了obj.text,副作用函数已经存进obj.text的依赖集合中,即使后面不读取,修改的话,依然可以从 依赖集合中拿到副作用函数来执行

解决方法就是在每次执行副作用函数之前,将副作用函数从相关依赖集合中移除,问题就迎刃而解 要想把一个副作用函数从所有与之相关联的依赖集合中移除,就需要知道哪些依赖集合含有它

    let activeEffect
    function effet(fn){
        const effectFn = ()=>{
            activeEffect = effectFn
            fn()
        }
        effectFn.deps=[] // activeEffect.deps用来存储与该副作用函数相关联的依赖集合
        effectFn() //执行副作用函数
    }
    
    function track(target,key){
        if(!active) 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中
        activeEffect.push(deps)
    }

有了这联系,根据effectFN.deps回去所有与之相关联的依赖集合,进而将副作用函数从中移除

    let activeEffect
    function effect(fn){
        const effectFn = ()=>{
            //调用cleanup函数完成去除工作
            cleanup(effectFn)
            activeEffect = effectFn
            fn()
        }
        effectFn.deps = []
        effectFn()
    }

cleanup函数实现

    function cleanup(effectFn){
        for(let i=0;i<effectFn.deps.length;i++){
            deps是当前副作用函数的依赖集合
            const deps = effectFn.deps[i]
            //将effectFn从依赖集合中去除
            deps.delete(effectFn)
        }
    }

但是,运行这段代码时候,会出现死循环

    function trigger(target,key){
        const depsMap = bucket.get(target)
        if(!depsMap) return
        const effects = depsMap.get(key)
        effects && effects.forEach(fn=>fn()) //问题就在这句代码中
    }

上面的代码执行,会调用副作用函数,副作用函数中会调用cleanup函数将依赖清除,但是副作用函数执行会读取到代理对象的属性,这会导致将副作用函数重新收集,对于effects来说,这个循环还在执行,因为这个依赖集合是Set类型,Set类型在遍历时,一个值被访问过,但该值被删除并重新添加到集合中去,如果此时遍历没有结束,那么值会被重新访问,一直循环下去,解决方法就是用另一个set集合去遍历

    function trigger(target,key){
        const depsMap = bucket.get(target)
        if(!depsMap) return
        const effects = depsMap.get(key)
        const effectsToRun = new Set(effects)
        effectsToRun.forEaach(fn=>fn())
    }

但是当我们的代码是这样

    effect(()=>{
        obj.foo = obj.foo+1
    })

在这个代码中,既会读取obj.foo的值,也会设置obj.foo的值,读取时触发tragger操作,将函数收集,更改obj.foo的值又会触发track操作,将副作用函数执行,问题是副作用函数正在执行,里面又有读取和设置的操作,就要开始下一次副作用函数执行,这就会导致无限递归调用自己 怎么解决?当副作用trigger触发的副作用函数和正在执行的副作用函数相同则不触发

    function trigger(target,key){
        const depsMap = bucket.get(target)
        if(!depsMap) return
        const effects = depsMap.get(key)
        const effectsToRun = new Set()
        effects && effects.forEach(effectFn=>{
            //如果trigger触发执行的副作用函数与当前执行的副作用函数相同,则不触发执行
            if(effectFn !== activeEffect){
                effectsToRun.add(effectFn)
            }
        })
        effectsToRun.forEaach(fn=>fn())
    }

5.嵌套的effect与effect栈

副作用函数会发生嵌套,

    effect(function effectFn1(){
        effect(function effectFn2(){...})
    })
    

什么时候会发生副作用函数的嵌套,在vue中渲染函数render是放在一个effect中执行的,当组件发生嵌套,副作用函数就会发生嵌套

    cosnt Foo={
        render(){
            return /../
        }
    }
    effect(()=>{
        Foo.render()
    })
    //当组件嵌套
    const Bar={
        render(){...}
    }
    const Foo={
        render(){
            return <Bar/>
        }
    }
    //此时的副作用函数
    effect(()=>{
        Foo.render()
        effect(()=>{
            Bar.render()
        
        })
    })

vue为什么要设计得可嵌套呢

    effect(function effectFn1(){
        console.log(1)
        effect(function effectFn2(){
            console.log(2)
            obj.bar
        })
        obj.fo
    })

在这段函数中我们修改obj.fo会发现输出为1,2,2,问题就在第3次输出,我们修改了obj.fo的值,应该打印的是1,但是打却是2,所以,这不符合预期 这个问题就出在effect 和activeEffect上

     let activeEffect
    function effect(fn){
        const effectFn = ()=>{
            //调用cleanup函数完成去除工作
            cleanup(effectFn)
            activeEffect = effectFn
            fn()
        }
        effectFn.deps = []
        effectFn()
    }

由上面的代码可知,我们是把副作用函数直接赋值给activeEffect,当函数发生嵌套,里层的函数会覆盖掉外层的函数,activeEffect收集的是最里面的副作用函数。 为了解决这个问题,vue设计了应该effectStack的函数栈,将当前的函数压入栈中,当副作用函数执行完再弹出,让activeEffect始终取到函数栈中最顶部的函数

     let activeEffect
     const effectStack=[]
    function effect(fn){
        const effectFn = ()=>{
            //调用cleanup函数完成去除工作
            cleanup(effectFn)
            activeEffect = effectFn
            effectStack.push(effectFn)//将副作用函数压入栈中
            fn()
            //副作用函数执行完,从栈中弹出
            effectStack.pop()
            activeEffect = effectStack[effectStack.length-1]
        }
        effectFn.deps = []
        effectFn()
    }