调度执行 与options 参数

可调度性是响应式数据系统的重要特性。

当trigger触发副作用函数时,我们希望用户有能力控制副作用函数执行的行为,以满足更多的需要。

我们为effect函数设置一个配置参数options,允许用户指定调度器。

effect(
    //第一个参数为注册的副作用函数
    () => {
        consoel.log(data.foo)
    },
    //第二个参数时是配置对象,允许用户设置调度器 scheduler
    {
        scheduler(fn){
            //...
        }
    }
)

在effect函数内部,我们只需要把options参数挂载到对应的副作用函数上。

function effect(fn,options){ //添加options形参
    function effectFunction() {
        cleanup(effectFunction)
        activeEffect = effectFunction
        effectStack.push(effectFunction)
        fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFunction.depsList = []
    effectFunction.options = options //将options挂载到effectFunction上
    effectFunction()
}

在trigerr函数中,我们把控制权交给options里的scheduler函数。

function trigger(target, key){
    let depsMap = bucket.get(target)
    if(!depsMap) return
    let deps = depsMap.get(key)
    if(deps.size === 0) return
    const depsToRun = new Set(deps) 
    console.log(depsToRun)
    depsToRun.forEach(fn =>{
        if(fn === effectFunction) return
        //如果effectFunction的options有scheduler函数,则调用这个函数;否则直接执行副作用函数。
        if(effectFunction.options.scheduler){
            effectFunction.options.scheduler(fn)
        }else{
           fn() 
        }
        
    });
}

有个这些基础,我们就可以尝试一些新的玩法了。考虑下面的例子:

effect(() => {
    console.log(data.value)
})
data.value++
data.value++

不出所料,代码执行的结果应该是先输出data.value的值,接着又连续输出两次data.value改变后的值。

我们希望,在响应式数据连续变化的过程中,只执行最后的副作用函数,而忽略变化过程中“过渡的”副作用函数。

因为浏览器的渲染只会下一个消息循环中执行,在一个消息任务中多余的副作用函数的执行是不必要的,我们只需要变化最后的结果一次触发响应式数据就好。

//定义一个消息队列
const jobQueue = new Set();//Set 的特性是自动去重,因此相同的任务不会被重复添加。
const p = Promise.resolve();//一个已经 resolve 的 Promise,用于将任务调度到微任务队列中执行。

let isFlushing = false;
function flushJob() {
    if(isFlushing === true) return // 如果正在刷新任务队列,直接返回
    isFlushing = true // 标记为正在刷新任务队列
    p.then(()=>{
        jobQueue.forEach(job => job())  // 执行所有任务
    }).finally(() => {
        isFlushing = false // 重置状态
    })
}

effect(
    () => {
    console.log(data.value);
    },
    {
        scheduler(fn) {
            jobQueue.add(fn)
            flushJob()
        }
    }
)

data.value++
data.value++

调度器把一个代码周期中的所有副作用函数都收集到jobQueue中,flushJob函数设置了一个标志位,保证在一个代码周期内无论调用多少次flushJob,只会执行一次。p开启了一个微队列,在下一个消息循环微队列开始执行,批量处理jobQueue中的副作用函数,并且没有函数重复。

这个功能有点类似于Vue.js中连续修改响应式数据但只会触发一次更新的功能,实际上Vue内部实现了一个更为完善的调度器,思路与上文相同。

计算属性computed 与 配置属性lazy

我们希望在注册副作用函数时不会上来就执行一次。

可以在options中添加lazy属性实现。

effect(
    () => {
        console.log(data.value);
    },
    // options
    {
        lazy: true
    }
)

function effect(fn,options){
    function effectFunction() {
        cleanup(effectFunction)
        activeEffect = effectFunction
        effectStack.push(effectFunction)
        fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFunction.depsList = []
    effectFunction.options = options
    //如果lasy为true,则不立即执行
    if(!options.lasy){
        effectFunction()
    }
    //返回封装的副作用函数,供用户主动调用
    return effectFunction
}

如果仅仅能够手动执行副作用函数,意义并不大。

如果我们传递给effect的函数看作一个getter,这getter可以返回任何值。

const effectFunction = effect(
    () => obj.foo + obj.bar
    // options
    { lazy: true }
)
// value是getter的返回值
const value = effectFunction()

function effect(fn,options){
    function effectFunction() {
        cleanup(effectFunction)
        activeEffect = effectFunction
        effectStack.push(effectFunction)
        const res = fn() // fn才是真正的副作用函数,也就是getter的返回值。
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
        return res //调用返回的effectFunction得到
    }
    effectFunction.depsList = []
    effectFunction.options = options
    //如果lasy为true,则不立即执行
    if(!options.lasy){
        effectFunction()
    }
    //返回封装的副作用函数,供用户主动调用
    return effectFunction
}

基于以上能够可以懒执行的副作用函数,我们可以构建计算属性computed函数了

function computed(getter) {
    const effectFunction = effect(getter,{lasy:true})
    
    // 立即返回一个对象,这个对象的value属性是一个访问器属性。
    // 有调用obj.value的值,才会执行effectFunction并返回结果。
    const obj = { 
        get value() {
            return effectFunction()
        }
    }
                 
    return obj
}

const data = { foo:1, bar:2}
const obj = reactive(data)

const sumRes = computed(
    ()=> obj.foo + obj.bar
)

console.log(sumRes.value)

当当当,comuted属性就完成了。

添加新评论