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中的入队方法:
ts
// 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
中,如下:ts// 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
:ts// 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)
-
异步组件加载时
ts// 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执行时
ts// 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
组件中父组件全部加载完之后ts// 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(热更新之后)
ts// 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
队列执行之后才会执行
ts
// 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
ts
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时才会执行
ts
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阶段也会被执行
ts
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介绍: cn.vuejs.org/api/general...
它同样写在了packages/runtime-core/src/scheduler.ts
文件中,它是通过什么才能确保在下一次更新时才执行回调的呢,是不是有这个疑问,下面就是它的源码:
ts
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会安排任务什么时候执行。。