美文网首页
vue.js设计与实现(四)-响应系统-分支切换与cleanup

vue.js设计与实现(四)-响应系统-分支切换与cleanup

作者: 幸宇 | 来源:发表于2022-12-29 15:25 被阅读0次

问题描述:在上一篇中,通过weakmap,map,Set数据结构,建立了代理对象中 target ,key, 副作用函数之前的对应关系,使得我们可以处理不存在的属性,避免不必要的程序执行,但上一篇的完善结构中还有一个问题出现,测试如下程序:

const data = {ok:true,text:'hello world'}
    let bucket = new WeakMap()
    let activeEffect;
    function effect(fn){
        activeEffect = fn;
        fn()
    }
    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)
    }

    function trigger(target,key){
        let depsMap = bucket.get(target)
        if(!depsMap) return
        const effects = depsMap.get(key)
        effects && effects.forEach(fn=>fn())
    }
    const obj = new Proxy(data,{
        get(target,key){
            track(target,key)
            return target[key]
        },
        set(target,key,newVal){
            target[key] = newVal;
            trigger(target,key)
        }
    })
    effect(
        function effectFn(){
            console.log('effect')
            document.body.innerText = obj.ok ? obj.text :'not' 
        }
    )
    setTimeout(()=>{
        obj.ok = false
        obj.text = 'hello vue3'
    })

完善结构后我们可以看到打印如下:


image.png

理论上在副作用函数中的判断后,我无论再去怎么改变obj.text的值,副作用函数都不应该再去执行一次,所以本次代码就是要解决如上问题;

解决思路很简单,在副作用函数执行前,删除所有与复函数关联的依赖属性即可

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

 function cleanup(effectFn){
        for(let i=0;i<effectFn.deps.length;i++){
            effectFn.deps[i].delete(effectFn)
        }
        // 重置数组
        effectFn.deps.length = 0
    }

track函数添加deps函数集合
    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)
        // 添加到deps数组中
        activeEffect.deps.push(deps)
    }

写到这里可以避免复函数的遗留了,但我们运行程序后会发现无线循环


image.png

问题出现在这里

01 function trigger(target, key) {
02   const depsMap = bucket.get(target)
03   if (!depsMap) return
04   const effects = depsMap.get(key)
05   effects && effects.forEach(fn => fn()) // 问题出在这句代码
06 }

解释:
在trigger函数中,effects存储在Set数据结构中,在遍历执行复函数时,cleanup会先删除当前复函数,然后在注册复函数的时候又把当前复函数添加到Set集合中,而此时Set集合还在遍历,就会导致无线循环,举例解释如下代码:

01 const set = new Set([1])
02
03 set.forEach(item => {
04   set.delete(1)
05   set.add(1)
06   console.log('遍历中')
07 })

语言规范中对此有明确的说明:在调用 forEach 遍历 Set 集合时,如果一个值已经被访问过了,但该值被删除并重新添加到集合,如果此时 forEach 遍历没有结束,那么该值会重新被访问。因此,上面的代码会无限执行。解决办法很简单,我们可以构造另外一个 Set 集合并遍历它:

01 const set = new Set([1])
02
03 const newSet = new Set(set)
04 newSet.forEach(item => {
05   set.delete(1)
06   set.add(1)
07   console.log('遍历中')
08 })

回到 trigger 函数,我们需要同样的手段来避免无限执行:

01 function trigger(target, key) {
02   const depsMap = bucket.get(target)
03   if (!depsMap) return
04   const effects = depsMap.get(key)
05
06   const effectsToRun = new Set(effects)  // 新增
07   effectsToRun.forEach(effectFn => effectFn())  // 新增
08   // effects && effects.forEach(effectFn => effectFn()) // 删除
09 }

整体代码构建如下:

const data = {ok:true,text:'hello world'}
    let bucket = new WeakMap()
    let activeEffect;
    function effect(fn){
        const effectFn = ()=>{
            cleanup(effectFn)
            activeEffect = effectFn;
            fn()
        }
        // activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
        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)
        // effects && effects.forEach(fn=>fn())
        effectsToRun.forEach(effectFn=>effectFn())
    }
    const obj = new Proxy(data,{
        get(target,key){
            track(target,key)
            return target[key]
        },
        set(target,key,newVal){
            target[key] = newVal;
            trigger(target,key)
        }
    })
    effect(
        function effectFn(){
            console.log('effect')
            document.body.innerText = obj.ok ? obj.text :'not' 
        }
    )
    setTimeout(()=>{
        obj.ok = false
        obj.text = 'hello vue3'
    })
    

结果查看:


image.png

相关文章

网友评论

      本文标题:vue.js设计与实现(四)-响应系统-分支切换与cleanup

      本文链接:https://www.haomeiwen.com/subject/xzetcdtx.html