Vue Scheduler

Vue中的Scheduler(任务调度系统),主要责任就是收集Vue在程序执行过程中产生的Jobs(任务),让每个Job按照固定的顺序执行,避免资源的浪费和程序中的循环依赖导致的死循环。

Vue Scheduler中的核心分为以下几个部分:

  • 任务队列(queue、pendingPostFlushCbs、activePostFlushCbs)
  • 任务入队程序(queueJob, queuePostFlushCb)
  • 任务执行状态(isFlushing、isFlushPending)
  • 当前正在执行的任务Promise(currentFlushPromise)
  • 任务执行程序(flushJobs、flushPostFlushCbs、flushPreFlushCbs)
  • 任务优先级调配程序(comparator)

源码位置:packages/runtime-core/src/scheduler.ts

任务队列

Vue Scheduler中的所有任务分为pre普通任务post三种显式优先级,以及根据id进行排序的隐式优先级(主要用作将父组件任务前置,确保组件的任务链不会乱序),如下所示:

  • pre任务普通任务放在queue
  • post任务放在pendingPostFlushCbs以及activePostFlushCbs

任务的添加

在Vue执行的不同阶段都有任务产生,一部分任务会根据当时的配置立即执行或某些条件下随parent.update执行,另一部分会被添加到任务调度系统中进行统一的调度,以下为Vue中的入队方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// packages/runtime-core/src/scheduler.ts
export function queueJob(job: SchedulerJob) {
if (
!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)
) {
if (job.id == null) {
queue.push(job)
} else {
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}

export function queuePostFlushCb(cb: SchedulerJobs) {
if (!isArray(cb)) {
if (
!activePostFlushCbs ||
!activePostFlushCbs.includes(
cb,
cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex
)
) {
pendingPostFlushCbs.push(cb)
}
} else {
pendingPostFlushCbs.push(...cb)
}
queueFlush()
}

queueJob

在任务进入queueJob时会进行查重的判断,不存在才会将任务push或insert进queue,同时由watch进入的Job是允许递归调用自身的,这时需要开发者自己思考好自己的代码是否会无限执行。

  • 在renderer.ts的setupRenderEffect函数(处理组件渲染的核心函数)中会创建update对象,在update执行时任务会被收集到queue中,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // packages/runtime-core/src/renderer.ts
    // create reactive effect for rendering
    const effect = (instance.effect = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(update),
    instance.scope // track it in component's effect scope
    ))
    // ...
    const update: SchedulerJob = (instance.update = () => effect.run())
    update.id = instance.uid
    // ...

    update()
  • 在this.$forceUpdate()执行时会被放入queue中,如下为Vue在this上拓展的属性其中就包含$forceUpdate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // packages/runtime-core/src/componentPublicInstance.ts
    export const publicPropertiesMap: PublicPropertiesMap =
    // Move PURE marker to new line to workaround compiler discarding it
    // due to type annotation
    /*#__PURE__*/ extend(Object.create(null), {
    $: i => i,
    $el: i => i.vnode.el,
    $data: i => i.data,
    $props: i => (__DEV__ ? shallowReadonly(i.props) : i.props),
    $attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),
    $slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots),
    $refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs),
    $parent: i => getPublicInstance(i.parent),
    $root: i => getPublicInstance(i.root),
    $emit: i => i.emit,
    $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
    $forceUpdate: i => i.f || (i.f = () => queueJob(i.update)),
    $nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
    $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
    } as PublicPropertiesMap)
  • 异步组件加载时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // packages/runtime-core/src/apiAsyncComponent.ts
    // ...
    load()
    .then(() => {
    loaded.value = true
    if (instance.parent && isKeepAlive(instance.parent.vnode)) {
    // parent is keep-alive, force update so the loaded component's
    // name is taken into account
    queueJob(instance.parent.update)
    }
    })
    .catch(err => {
    onError(err)
    error.value = err
    })
    // ...
  • Watch执行时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // packages/runtime-core/src/apiWatch.ts
    // ...
    let scheduler: EffectScheduler
    if (flush === 'sync') {
    scheduler = job as any // the scheduler function gets called directly
    } else if (flush === 'post') {
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
    } else {
    // default: 'pre'
    job.pre = true
    if (instance) job.id = instance.uid
    scheduler = () => queueJob(job)
    }
    // ...
  • hmr(开发环境热更新)时也会执行queueJob

queuePostFlushCb

在Job进入queuePostFlushCb时也会进行判断,此时判断的是任务是否在activePostFlushCbs队列中,即是否将要执行,如果不存在即将执行的任务队列则添加到pendingPostFlushCbs队列

  • 在组件的mount、update、unmount等生命周期时都会执行queuePostFlushCb添加后置任务

    点太多了,都粘过来有点多余了,具体详见源码: packages/runtime-core/src/renderer.ts

  • Suspense组件中父组件全部加载完之后

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // packages/runtime-core/src/components/Suspense.ts
    // ...
    // flush buffered effects
    // check if there is a pending parent suspense
    let parent = suspense.parent
    let hasUnresolvedAncestor = false
    while (parent) {
    if (parent.pendingBranch) {
    // found a pending parent suspense, merge buffered post jobs
    // into that parent
    parent.effects.push(...effects)
    hasUnresolvedAncestor = true
    break
    }
    parent = parent.parent
    }
    // no pending parent suspense, flush all jobs
    if (!hasUnresolvedAncestor) {
    queuePostFlushCb(effects)
    }
  • hmr(热更新之后)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // packages/runtime-core/src/hmr.ts
    // 5. make sure to cleanup dirty hmr components after update
    queuePostFlushCb(() => {
    for (const instance of instances) {
    hmrDirtyComponents.delete(
    normalizeClassComponent(instance.type as HMRComponent)
    )
    }
    })

ssr 相关的没列

任务优先级

Vue中的scheduler通过comparator给queue队列中的pre任务普通任务进行排序,而pendingPostFlushCbs队列则存放post任务一定在queue队列执行之后才会执行

1
2
3
4
5
6
7
8
9
10
// packages/runtime-core/src/scheduler.ts
const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
const diff = getId(a) - getId(b)
// 如果id相同根据是否是pre进行排序,pre优先于普通任务, 如果id不同则根据从小到大排序,确保parent的任务优于children的任务执行
if (diff === 0) {
if (a.pre && !b.pre) return -1
if (b.pre && !a.pre) return 1
}
return diff
}

任务执行分析

任务通过以下三个函数执行:

flushJobs

这个函数最初由queueJob开始触发执行queueFlush,包装一层Promise.resolve后执行flushJobs

此处主要分析flushJobs函数,其余源码请看:packages/runtime-core/src/scheduler.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function flushJobs(seen?: CountMap) {
// 设置任务状态为Flushing(已完成)
isFlushPending = false
isFlushing = true
// ...
queue.sort(comparator)
// ...
try {
// 此处按优先级执行任务Job
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
// 判断Job是否失活,比如任务执行前卸载了组件,就会跳过这个任务
if (job && job.active !== false) {
// ...
// 由统一的错误处理程序执行job,可以统一报错
// 具体声明详见: packages/runtime-core/src/errorHandling.ts
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
// 将标志及队列初始化
flushIndex = 0
queue.length = 0
// 执行后置任务
flushPostFlushCbs(seen)
// 初始化任务状态及任务当前的Promise
isFlushing = false
currentFlushPromise = null
// 如果queue或pendingPostFlushCbs中还存在任务则递归调用当前函数
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}

flushPreFlushCbs

这个函数只会在组件render和updateComponentPreRender时才会执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export function flushPreFlushCbs(
seen?: CountMap,
// if currently flushing, skip the current job itself
i = isFlushing ? flushIndex + 1 : 0
) {
// ...
// 从标识位开始执行,标识位之前的是已经执行过的
for (; i < queue.length; i++) {
const cb = queue[i]
// 如果pre属性存在则为前置任务,执行完删除,同时标志位向前挪一位(不挪会导致少执行一个任务)
if (cb && cb.pre) {
// ...
queue.splice(i, 1)
i--
cb()
}
}
}

flushPostFlushCbs

这个函数会执行后置任务即post任务,除在flushJobs中执行完queue队列后执行外,在组件render阶段也会被执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
export function flushPostFlushCbs(seen?: CountMap) {
// 判断post任务的队列是否有任务
if (pendingPostFlushCbs.length) {
// 进行去重,同时清空post队列
const deduped = [...new Set(pendingPostFlushCbs)]
pendingPostFlushCbs.length = 0

// 判断是否存在正在执行的后置任务队列即activePostFlushCbs队列,存在则直接加到队列后边
if (activePostFlushCbs) {
activePostFlushCbs.push(...deduped)
return
}
// 将pendingPostFlushCbs队列内的任务去重后赋值给正在执行的队列activePostFlushCbs队列
activePostFlushCbs = deduped
// ...
// 根据id排序,确保parent任务优先执行
activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
// 按顺序执行任务
for (
postFlushIndex = 0;
postFlushIndex < activePostFlushCbs.length;
postFlushIndex++
) {
// ...
activePostFlushCbs[postFlushIndex]()
}
// 初始化正在执行的后置任务队列activePostFlushCbs
// 初始化后置任务标志位
activePostFlushCbs = null
postFlushIndex = 0
}
}

nextTick

作为Vue中的一个全局API,它的作用是等待下一次 DOM 更新刷新的工具方法.

API介绍: https://cn.vuejs.org/api/general.html#nexttick

它同样写在了packages/runtime-core/src/scheduler.ts文件中,它是通过什么才能确保在下一次更新时才执行回调的呢,是不是有这个疑问,下面就是它的源码:

1
2
3
4
5
6
7
export function nextTick<T = void, R = void>(
this: T,
fn?: (this: T) => R
): Promise<Awaited<R>> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}

它就是在任务调度系统中维护的那个currentFlushPromise当前任务的Promise之后加了个.then的微任务,由于任务队列中的任务都会在currentFlushPromise的第一个.then中执行,所以nextTick才能在那些任务之后执行。

总结

scheduler的作用就是接收其他的函数丢过来的任务,然后把他们执行掉

别人只管丢任务给总部,不用管任务什么时候做,就好比我们的排期,产品只管丢给actionView,actionView会安排任务什么时候执行。。