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

52 阅读3分钟

6.调度执行

可调度执行是响应式系统重要的特性。什么是可调度,就是当trigger动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机,次数以及方式 应该怎么决定副作用函数的执行方式

    const data = {foo:1}
    const obj = new Proxy(data,{...})
    effect(()=>{
        console.log(obj.foo)
    })
    obj.foo++
    console.log('结束了')

这段代码输出结果

    1
    2
    '结束了'

如果我们要将输出顺序调整为

    1
    '结束了'
    2

很简单,只需要在代码中把obj.foo++和console.log()调换位置就行了,但是响应系统就是为了不调整代码情况下实现这个需求,这时候就要让响应系统支持调度 Vue在effect函数中设计了一个选项函数options,允许用户指定调度器

    effect(()=>{
          console.log(obj.foo)  
        },//options
        {   //调度器scheduler是一个函数
            scheduler(fn){
                ...
            }
        }
    )

由上面可知,vue允许用户在调用effect函数注册副作用函数时,传递第二个参数options,允许指定scheduler调度函数,同时在effect函数内部我们需要把options选项挂载到对应副作用函数上

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

有了调度函数,在trigger函数中触发副作用函数重新执行时,就可以直接调用用户的调度器函数,把控制权交给用户

    function trigger(target,key){
        ...
        effectsToRun.forEach(effectFn=>{
            if(effectFn.options.scheduler){
                effectFn.options.scheduler(effectFn)
            }else{
                effectFn()
            }
        })
    }

有了前面的代码铺路后,我们需求就可以实现了

    effect(()=>{
            console.log(obj.foo)
        },
        {
            scheduler(fn){//将副作用函数放到宏任务队列里面
                setTimeout(fn)
            }
        }
    )
    obj.foo++
    console.log('结束了')

调度函数除了控制副作用函数执行顺序,还可以控制副作用函数执行次数,如果代码是这样的

    effec(()=>{
        console.log(obj.foo)
    })
    obj.foo+
    obj.foo+
    
    //1
    //2
    //3

我们可以知道,obj.foo一定会自增到3,2只是过渡状态,我们只关心结果不关心过程,那么就不需要打印3次我们希望的结果是1 3就行了,基于调度器我们就能实现这个需求

    const jobQueue = new Set() //定义一个任务队列
    const p = Promise.resolve*(//
    //一个标志代表正在刷新队列
    let isFlushing = false
    function flushJob(){
        //如果队列正在刷新,则什么都不做
        if(isFlushong) retun
        //设置为true代表正在刷新
        //在微任务队列中刷新jobQueue队列
        p.then(()=>{
            jobQueue.forEach(job=>job())
        }).finally(()=>{
            isFlushing = false
        })
    }
    
    effect(()=>{
        console.log(obj.foo)
    },{
        scheduler(fn){
        //每次调度,将副作用函数添加到jobQueue队列中
            jobQueue.add(fn)
            //调用flushJob刷新队列
            flushJob
        }
    })
    obj.foo++
    obj.foo++

上面这段代码,就是在调度器函数中,将副作用函数添加至jobQueue中,我们执行了两次++操作,副作用函数添加了2次,利用set自动去重的功能,最终jobQueue只有一项,我们是在微任务队列中执行副作用函数,我们已经++操作完了,我们队列里面只有1项此时obj.foo已经是3了只会执行最后一次副作用函数