美文网首页
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