3.3 具有调度功能的响应式设计

前文提要:
3.0 响应式系统的设计与实现*
3.1 一个稍微完善的Vue.js响应式系统
3.2 继续完善的Vue.js响应式系统

1、关于调度器的理解

从上述的章节中我们设计了一套响应式系统,但是这个系统有个问题是副作用函数是不可控的,即我们无法控制副作用函数的执行时机和执行次数。只能做到简单的触发-执行这样的流程。而调度器在系统设计中是非常常用的,一个系统运行其实就是一个一个功能的重复执行,而调度器的设计主要实现两个目的:

  1. 实现调度策略的灵活配置
  2. 资源的有效利用

在Vue.js的响应式系统中的副作用函数调度策略也是围绕这两点来设计的。

2、调度器的设计

假设有如下代码

// 省略部分代码
effect(() => {
    console.log(obj.foo)
})
obj.foo++
console.log('结束啦')

此时的输出顺序为

1
2
结束啦

但是如果需要将输出的顺序改变为

1
结束啦
2

此时就需要调度一下副作用函数的执行时机,让输出顺序和上面提出的需求顺序相同。其实原理很简单,就是将需要延迟执行的部分放入微任务队列就行了,那么我们怎么设计调度器来执行这一过程呢,代码可以如下修改:

function effect(fn, options = {}) {
    const effectFn = () => {
    	// 省略代码...
    }
	// 设置option选项,可以将调度器放入
    effectFn.options = options
    effectFn.deps = []
    effectFn()
}

function trigger(target, key) {
    // 省略代码 ... 
    
    // 将副作用函数追踪下来,防止出现set在删除时插入新值
    effectsToRun.forEach(effectFn => {
        // 检查是否有调度器,有调度器则执行调度器
        if(effectFn.options.scheduler) {
            effectFn.options.scheduler(effectFn)
        } else {
            effectFn()
        }
    })
} 

effect(() => {
    console.log(obj.foo)
}, { 
	// 设定调度函数
    scheduler: (effectFn) => {
    	// 放入宏任务队列中,调整执行顺序
        setTimeout(effectFn)
    }
})

这样给effectFn的参数中设置调度函数,通过调度函数设置执行的时机。

3、使用调度器节省副作用函数执行的资源消耗

在面试中常问的一个问题是,如响应式数据 x绑定了副作用函数,要是循环执行x++一万次,那么其副作用函数是否也会执行一万次。答案根据我们的直觉来说肯定是否定的,但是原理是什么呢?其实就是调度器的设置。

我们根据以上的代码测试一下

effect(() => {
    console.log(obj.foo)
})
obj.foo++
obj.foo++
obj.foo++
console.log('结束啦')

输出的有

1
2
3
4
结束啦

其实这些输出结果中的2、3都是过渡状态,是没必要执行的,这些中间状态的控制用调度器是很容易实现的,在Vue中使用的是一个副作用函数的任务执行队列(Set模拟),将要执行的副作用函数放入任务队列,但是任务队列的执行是在微任务队列中,这样就如上述所说的调整了执行的顺序。

所有过度状态的副作用函数会先触发,将要执行的副作用函数放入Set中,由于Set的自动去重机制会将重复的副作用函数去掉,这样就省略了过度状态,具体代码如下:

// 省略代码 ......

//副作用函数任务的队列
const jobQueue = new Set()
// 一个标记是否正在刷新队列
let isFlushing = false
// 将任务添加到微任务队列中
const p = Promise.resolve()

function flushJobs() {
    // 如过正在执行副作用个函数队列直接跳出
    if(isFlushing) return
    // 标记队列执行的状态
    isFlushing = true

    p.then(() => {
        jobQueue.forEach(job => job())
    }).finally(() => {
        // 执行完之后改变状态标记
        isFlushing = false
    })
}

effect(() => {
    console.log(obj.foo)
}, {
    scheduler: (effectFn) => {
        // 添加到副作用函数任务队列中
        jobQueue.add(effectFn)
        // 刷新队列
        flushJobs()
    }
})

obj.foo++
obj.foo++
obj.foo++

其实这只是一个简单的调度器,主要是为了展示调度器的功能,即调整执行时机和节省资源,而Vue的完整调度器更加的完善和复杂。

你可能感兴趣的:(javascript,开发语言,ecmascript,vue.js,前端)