在学习了computed和watch的代码之后,我们会发现,computed和watch中都包含调度器scheduler的概念。完整的来说,它应该是调度系统;
调度系统包含两部分实现:
- lazy:懒执行;
- scheduler:调度器;
懒执行
参考vue3源码packages/reactivity/src/effect.ts中183-185行:
ts
// 如果lazy是真,不会直接执行run函数;
// 当我们setter触发时,才会执行watch
if (!options || !options.lazy) {
_effect.run()
}
在我们的项目vue-mini中的effect.ts修改:
ts
export interface ReactiveEffectOptions {
lazy?: boolean;
scheduler?: EffectScheduler;
}
export function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions) {
const _effect = new ReactiveEffect(fn);
if (!options || !options.lazy) {
_effect.run();
}
}
修改完成之后,写一个测试实例;
js
<script>
const { reactive, effect } = Vue;
const obj = reactive({
count: 1,
});
effect(
() => {
console.log(obj.count);
},
{
lazy: true,
}
);
obj.count = 2;
console.log("代码结束");
</script>
当lazy是true的时候,只打印出代码结束;
当lazy是false的时候,打印出1和2:
调度器
调度器分为两个部分:
- 控制执行顺序;
- 控制执行规则;
控制执行顺序
我们上面的测试实例:
js
<script>
const { reactive, effect } = Vue;
const obj = reactive({
count: 1,
});
effect(
() => {
console.log(obj.count);
},
);
obj.count = 2;
console.log("代码结束");
</script>
最后打印输出的顺序如下图所示:
如果我们想要输出的顺序变成下面这样呢?
1
代码结束
2
是否能用scheduler做到?我们来创建一个测试实例运行一下!
ts
const { reactive, effect } = Vue
const obj = reactive({
count: 1
})
effect(
() => {
console.log(obj.count)
},
{
scheduler() {
setTimeout(() => {
console.log(obj.count)
})
}
}
)
obj.count = 2
console.log('代码结束')
打印的顺序就变成了1、代码结束、2了!
这是为什么呢? 会想到我们自己实现的triggerEffect方法,其中有scheduler的判断;当我们在watch中传入scheduler时就不会去执行run方法了,scheduler方法是一个异步任务,只有等所有的同步任务完成后才会触发;也就是说scheduler会影响代码的执行顺序:
js
// 触发指定依赖
export function triggerEffect(effect: ReactiveEffect) {
// 判读是否有scheduler
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
那现在为我们的项目vue-mini去增加scheduler相关的逻辑; 在shared/index.ts中增加一个extend方法;
ts
export const extend = Object.assign;
修改effect.ts方法,增加合并_effect和options的逻辑:
ts
export function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions) {
const _effect = new ReactiveEffect(fn);
if (options) {
// 合并_effect和options
extend(_effect, options);
}
if (!options || !options.lazy) {
_effect.run();
}
}
运行上面相同的实例,会发现打印的顺序改变了;
ts
const { reactive, effect } = Vue
const obj = reactive({
count: 1
})
effect(
() => {
console.log(obj.count)
},
{
scheduler() {
setTimeout(() => {
console.log(obj.count)
})
}
}
)
obj.count = 2
console.log('代码结束')
控制执行规则
创建一个新的测试实例:
ts
const { reactive, effect } = Vue
const obj = reactive({
count: 1
})
effect(() => {
console.log(obj.count)
})
obj.count = 2
obj.count = 3
这次打印出1、2、3;最后obj.count都会被修改成3,那我们能跳过2的流程吗?在vue3的源码中scheduler.ts文件中有一个queuePreFlushCb方法,它是用来控制scheduler的执行规则的,现在我们来看一下它是怎么运行的;
首先要去vue3源码中export queuePreFlushCb方法,重新build一遍;然后修改我们的测试实例,再运行一下;
ts
const { reactive, effect, queuePreFlushCb } = Vue
const obj = reactive({
count: 1
})
effect(
() => {
console.log(obj.count)
},
{
scheduler() {
queuePreFlushCb(() => console.log(obj.count))
}
}
)
obj.count = 2
obj.count = 3
queueFlush方法是关键;
ts
function queueFlush() {
// 此时isFlushing和isFlushPending都为false
if (!isFlushing && !isFlushPending) {
isFlushPending = true
// resolvedPromise是Promise.resolve(),flushJobs都变成异步微任务;
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
接着我们来实现scheduler的控制执行规则功能; 新建packages/runtime-core/src/scheduler.ts;
ts
let isFlushPending = false;
const pendingPreFlushCbs: Function[] = [];
let currentFlushPromise: Promise<void> | null = null;
const resolvedPromise = Promise.resolve() as Promise<any>;
export function queuePreFlushCb(cb: Function) {
queueCb(cb, pendingPreFlushCbs);
}
export function queueCb(cb: Function, pendingQueue: Function[]) {
pendingQueue.push(cb);
queueFlush();
}
export function queueFlush() {
if (!isFlushPending) {
isFlushPending = true;
currentFlushPromise = resolvedPromise.then(flushJobs);
}
}
function flushJobs() {
isFlushPending = false;
flushPostFlushCbs();
}
export function flushPostFlushCbs() {
if (pendingPreFlushCbs.length) {
let activePostFlushCbs = [...new Set(pendingPreFlushCbs)];
pendingPreFlushCbs.length = 0;
for (
let postFlushIndex = 0;
postFlushIndex < activePostFlushCbs.length;
postFlushIndex++
) {
activePostFlushCbs[postFlushIndex]();
}
}
}
记得还有两个地方需要export queuePreFlushCb方法;