Vue3中数据更新函数effect的实现

数据更新函数effect的实现

基本思路
  • reactive函数对数据进行proxy劫持
  • 调用effect函数,传入用户定义函数
  • 用户定义函数会自执行一次,其内存在对数据的调用
  • 对数据的调用会触发proxy接触
    • 如果是触发get,则把当前触发的属性和当前effect绑定
    • 如果触发set,则把当前属性绑定的effect取出,并调用,使之进行数据更行
 const {effect,reactive } = VueReactivity
        //对数据进行绑定
        const obj = {
            name:'sx',
            age:13,
            address:{
                num:30
            },
            flag:true
        }
        //这里只能传入对象,因为proxy只支持对象格式
        const state = reactive(obj)
        // const state2 = reactive(state)
        //数据响应
        //effect函数会默认执行一次,后续数据发生变化会重新执行effect函数
        effect(()=>{
            app.innerHTML = state.name+'今年'+state.age+'岁了门牌号是'+state.address.num
        })
        setTimeout(()=>{
            state.age++
        },1000)
实现细节
ReactiveEffect类

ReactiveEffect类是effect的构造函数,其内部有控制传入的函数执行的函数run

  • 声明effect函数,实质是在内部实例化一个ReactiveEffect对象,并调用其run函数实现初次effect的执行,从而触发proxy,
export function effect(fn){
    const _effect = new ReactiveEffect(fn)
    _effect.run()
}
  • run函数的作用不仅是执行其传入的回调,触发proxy,同时会将上下文暴漏给外部,赋值给activeEffect,由于js执行机制为单线程,因此当暴漏出指针后,触发proxy的get或者set,利用track函数进行依赖收集
 run(){
        //依赖收集,让属性和effect产生关联
        //如果没有激活,则不进行依赖收集
        if(!this.active){
            return this.fn()
        }else {
           try{
               //让activeEffect指向当前effect,
                activeEffect = this
                //触发react中的get或set
                return this.fn()
           }
           finally{
                activeEffect = undefined
           }
        }
    }
依赖收集函数track
  • 当触发proxy中的get,会调用依赖收集函数track,收集属性对应哪个effect,主要格式为:obj -> key -> effect。
const targetMap = new weakMap()
export function track(target,key){
    if(activeEffect){
       //判断是否存在该对象的键值
       let depsMap = targetMap.get(target)
       if(!depsMap){
            targetMap.set(target,(depsMap = new Map()))
       }
       //判断是否存在该属性的键值
        let deps = depsMap.get(key)
        if(!deps){
            depsMap.set(key,(deps = new Set()))
        }
        trackEffects(deps)
    }

}
export function trackEffects(deps){
    let shouldTrack = !deps.has(activeEffect)
    if(shouldTrack){
        deps.add(activeEffect)
    }
    // 在ReactiveEffect中声明公有变量deps,用来存储属性对应的集合deps
    activeEffect.deps.push(deps)
}
触发更新函数trigger
  • 触发更新函数trigger在proxy的set中,当传进来的新值不等于旧值时,执行set的赋值操作,并触发trigger,trigger函数中会取出该属性所依赖的effect,依次执行其中的run函数,这样就完成了数据的更新
export function trigger(target,key,value){
    let depMaps = targetMap.get(target)
    if(!depMaps){
        return //没有依赖收集
    }
    let effects = depMaps.get(key)
    triggerEffects(effects)
}

export function triggerEffects(effects){
    if(effects){
        effects.forEach(effect=>{
            effect.run()
        })
    }
}
细节完善

至此,我们根据最初的范例,已经能够实现数据改变,页面更新的效果,但是依然还有一些细节需要完善

  • effect的嵌套问题
  effect(()=>{
      effect(()=>{
                state.age = 18
            })
            app.innerHTML = state.name+'今年'+state.age+'岁了门牌号是'+state.address.num
          
        })

我们通过用activeEffect记录effect内部实例的方式来暴漏出effect,从而实现依赖收集,到那时effect的run函数执行完之后,activeEffect会赋值为undefined,这就暴漏一个问题,activeEffect起初指向外层effect,然后指向内层effect,再然后执行完内层effect被赋值为undefined,但是外层还没有进行依赖收集,此时进行依赖收集将无法找到绑定的effect

这种嵌套的调用类似一种树状结构,因此我们可以用activeEffect记录当前环境,当环境改变,记录其parent即可

run(){
    if(!this.active){
        return this.fn()
    }else {
        try{
            this.parent = activeEffect
            activeEffect = this
            return this.fn()
        }
        finally{
            activeEffect = this.parent
            this.parent = null
        }
    }
}
  • effect调用自己的问题
 effect(()=>{
            state.age = Math.random()
            app.innerHTML = state.name+'今年'+state.age+'岁了门牌号是'+state.address.num
          
        })
        setTimeout(()=>{
            state.age++
        },1000)

当effect完成依赖收集后,调用setTimeout函数,触发proxy的set,从而触发trigger更新,但是触发的过程中遇到 app.age = Math.random() ,会重复触发,使程序停不下来

这种明显是我们不想看到的,因此再触发的时候可以进行判断,如果触发的effect和当前的activeEffect相同,则不进行更新操作

effects.forEach(effect=>{
    if(effect !== activeEffect){
        effect.fn()
    }
})
  • 清除多余依赖
  effect(() => {
            console.log('render')
            app.innerHTML = state.flag ? state.name + '今年' + state.age + '岁了门牌号是' + state.address.num : 'hello world'
        })
        setTimeout(() => {
            state.flag = false
            setTimeout(() => {
                state.age++
            }, 1000);
        }, 1000)

effect进行依赖收集之后,调用外面的定时器,会将数据隐藏,此时再调用里面定时器改变age的值,页面依然刷新,这对性能印象很大

解决这个问题就需要对依赖进行清除,当之前run函数进行依赖收集之前,将属性对该effect产生的依赖进行清除

function cleanEffect(effect){
    let deps = effect.deps
    for(let i = 0;i<deps.length;++i){
        deps[i].delete(effect)
    }
    effect.deps.length = 0
}

这样并不能解决问题,反而造成程序死循环,原因就是在进行trigger更新的时候,会循环遍历effect,依次执行run,再run 中又会cleaneffect依赖,重新收集依赖,从而造成死循环
要解决这个方法只需要trigger时,新建一个effects集合即可

if(effects){
    effects = new Set(effects)
    effects.forEach(effect=>{
         if (effect !== activeEffect) { // 保证要执行的effect不是当前的effect
                    effect.run(); // 数据变化了,找到对应的effect 重新执行
                
            }
    })
}
  • effect返回值

effect可以返回一个值runner,其包含了停止更新的函数stop,也可以手动控制更新runner()

  • ReactiveEffect中新增一个stop函数
  stop(){
        if(this.active){
            this.active = false
        }
        cleanEffect(this)
    }
  • effect中新增返回值runner
export function effect(fn){
    const _effect = new ReactiveEffect(fn)
    _effect.run()
    let runner = _effect.run.bind(_effect)
    runner.effect = _effect
    return runner
}
  let runner = effect(() => {
            console.log('render')
            app.innerHTML = state.flag ? state.name + '今年' + state.age + '岁了门牌号是' + state.address.num : 'hello world'
        })
        runner.effect.stop()
        setTimeout(() => {
            state.flag = false
            setTimeout(() => {
                state.age++
            }, 1000);
        }, 1000)

可以看到,调用stop后,数据不再更新,重新调用runner,页面继续更新

  let runner = effect(() => {
            console.log('render')
            app.innerHTML = state.flag ? state.name + '今年' + state.age + '岁了门牌号是' + state.address.num : 'hello world'
        })
        runner.effect.stop()
        setTimeout(() => {
            
            state.flag = false
            runner()
        }, 1000)
  • 更新调度函数的实现
 let runner = effect(() => {
            console.log('render')
            app.innerHTML = state.flag ? state.name + '今年' + state.age + '岁了门牌号是' + state.address.num : 'hello world'
        })
        setTimeout(() => {
            state.age++
            state.age++
            state.age++

        }, 1000)

这里页面会渲染三次,而不会是等age全部更新完渲染一次,可以采用promise异步来做调度

 let runner = effect(() => {
            console.log('render')
            app.innerHTML = state.flag ? state.name + '今年' + state.age + '岁了门牌号是' + state.address.num : 'hello world'
            if(flag){
                flag = false
            
                Promise.resolve().then(()=>{
                    runner()
                })
            }
        })
        setTimeout(() => {
            state.age++
            state.age++
            state.age++

        }, 1000)

effect同样自身也实现了调度函数,即effect可以传递第二个参数,为一个对象,对象中如果有scheduler函数,则数据变化执行scheduler函数,如果没有,则执行effect.run

    let flag = true
      let runner = effect(() => {
            console.log('render')
            app.innerHTML = state.flag ? state.name + '今年' + state.age + '岁了门牌号是' + state.address.num : 'hello world'
           
        },{
            scheduler(){
                if(flag){
                flag = false
            
                Promise.resolve().then(()=>{
                    runner()
                })
            }
            }
        })
        setTimeout(() => {
            state.age++
            state.age++
            state.age++

        }, 1000)

在triggerEffects函数中进行判断

export function triggerEffects(effects) {
   
    if(effects){
        effects = new Set(effects)
        effects.forEach(effect =>{
            if(effect !== activeEffect){
                if(effect.scheduler){
                    effect.scheduler()
                }else{
                    effect.run()
                }
            
            }
        })
    }
}
  • 对引用类型进行数据绑定

通过proxy进行绑定的数据只是对obj最外层做代理,里面不会被监控到

    let runner = effect(() => {
            console.log('render')
            app.innerHTML = state.flag ? state.name + '今年' + state.age + '岁了门牌号是' + state.address.num : 'hello world'
           
        },{
            scheduler(){
                if(flag){
                flag = false
            
                Promise.resolve().then(()=>{
                    runner()
                })
            }
            }
        })
        setTimeout(() => {
          state.address.num = 40

        }, 1000)

如上,改变state.address.num不会触发更新,但是在访问到state.address时,会触发,只需要在触发的时候做一层判断即可

 get(target,key,receiver){
        if(key === ReactiveFlags.IS_REACTIVE){
            return true
        }
       track(target,key)
        
        let res = Reflect.get(target,key,receiver)
        if(isObject(res)){
            return reactive(res)
        }
        return res
    },
ss.num不会触发更新,但是在访问到state.address时,会触发,只需要在触发的时候做一层判断即可
```js
 get(target,key,receiver){
        if(key === ReactiveFlags.IS_REACTIVE){
            return true
        }
       track(target,key)
        
        let res = Reflect.get(target,key,receiver)
        if(isObject(res)){
            return reactive(res)
        }
        return res
    },
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

问也去

创作不易,感谢支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值