Vue 3 的调度器(scheduler)内部维护了多个任务队列,用于控制副作用(effects)的执行时机。
前置任务队列
执行时机:在组件更新之前执行
前置任务存在的基础任务队列中
flushPreFlushCbs 执行前置任务
执行主任务中含有前置标识的任务 SchedulerJobFlags.PRE。
js
function flushPreFlushCbs(
instance?: ComponentInternalInstance,
seen?: CountMap,
// skip the current job
i: number = flushIndex + 1,
): void {
if (__DEV__) {
seen = seen || new Map()
}
// 从索引 i 开始遍历 queue 队列
for (; i < queue.length; i++) {
const cb = queue[i]
// 如果任务有 PRE 标志,说明是前置任务
if (cb && cb.flags! & SchedulerJobFlags.PRE) {
// 任务的 id 与实例的 uid 不同,则跳过该任务
if (instance && cb.id !== instance.uid) {
continue
}
if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
continue
}
// 从队列中移除任务:使用 splice 从队列中移除当前任务
queue.splice(i, 1)
// 调整索引:由于队列长度减少,需要将索引 i 减 1
i--
if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
cb.flags! &= ~SchedulerJobFlags.QUEUED
}
// 执行任务:直接调用任务函数
cb()
if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
cb.flags! &= ~SchedulerJobFlags.QUEUED
}
}
}
}
js
// 存储待执行的基本任务(主任务+前置任务)
const queue: SchedulerJob[] = []
在 render 中执行 flushPreFlushCbs

在 updateComponentPreRender 组件渲染前 执行

示例 watch pre


基础任务队列 queue
包含主任务和前置任务。
执行时机:组件实际的渲染/更新 effect,以及用户通过 queueJob 添加的普通 job
queueJob 加入主任务队列
js
function queueJob(job: SchedulerJob): void {
// 如果任务未被标志为QUEUED,则执行入队操作
if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
const jobId = getId(job) // 获取任务的唯一标识符
const lastJob = queue[queue.length - 1] // 获取当前队列中的最后一个任务
// 入队策略:
// 快速路径:如果队列为空,或者任务没有 PRE 标志且任务 ID 大于等于队尾任务的 ID,则直接将任务推入队列末尾
if (
!lastJob ||
(!(job.flags! & SchedulerJobFlags.PRE) && jobId >= getId(lastJob))
) {
queue.push(job)
// 插入路径:否则,通过 findInsertionIndex 找到合适的插入位置,使用 splice 插入任务
} else {
queue.splice(findInsertionIndex(jobId), 0, job)
}
job.flags! |= SchedulerJobFlags.QUEUED // 标记任务为已入队
queueFlush() // 执行
}
}
js
// 存储待执行的主任务
const queue: SchedulerJob[] = []
queueFlush
准备微任务执行 flushJobs
js
function queueFlush() {
// 检查是否已经有正在执行的刷新操作
// 这是一个防重复机制,确保同一时间只触发一次刷新
if (!currentFlushPromise) {
// 在 Promise 解析后执行 flushJobs 函数
// flushJobs 函数负责执行所有任务,包括 DOM 更新操作
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
resolvedPromise是Promise.resolve(),因此flushJobs会在当前事件循环的微任务阶段执行。- 这保证了在同步代码全部执行完毕后,才开始 DOM 更新。
js
// 一个已解决的 Promise 实例
// 用于创建微任务,将任务延迟到下一个微任务执行
const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise<any>
flushJobs 执行主任务
先执行任务队列,再执行后置任务队列。
js
function flushJobs(seen?: CountMap) {
if (__DEV__) {
seen = seen || new Map()
}
const check = __DEV__
? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
: NOOP
try {
// 1、执行主任务队列:按照顺序执行 queue 中的所有任务
// 遍历队列中的任务,执行每个任务
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
// 任务未被设置为已销毁
if (job && !(job.flags! & SchedulerJobFlags.DISPOSED)) {
if (__DEV__ && check(job)) {
continue
}
if (job.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
job.flags! &= ~SchedulerJobFlags.QUEUED // 清除 QUEUED 标志
}
// 执行任务:如果任务没有被标记为 DISPOSED,执行任务
callWithErrorHandling(
job,
job.i,
job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER,
)
if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
job.flags! &= ~SchedulerJobFlags.QUEUED // 清除 QUEUED 标志
}
}
}
} finally {
// If there was an error we still need to clear the QUEUED flags
for (; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job) {
job.flags! &= ~SchedulerJobFlags.QUEUED // 清除 QUEUED 标志
}
}
// 重置索引:将 flushIndex 重置为 -1,表示队列执行完成
flushIndex = -1
// 清空队列:将 queue 长度设置为 0,清空主任务队列
queue.length = 0
// 调用 flushPostFlushCbs 执行后置任务队列
flushPostFlushCbs(seen)
currentFlushPromise = null
// If new jobs have been added to either queue, keep flushing
// 新任务:如果执行过程中又有新任务加入队列,继续执行
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}
示例 组件执行更新

后置任务队列 pendingPostFlushCbs
执行时机:在组件更新之后执行
queuePostRenderEffect 加入后置任务队列
queuePostRenderEffect 函数的作用是将一个副作用(side effect)回调放入"渲染后"的队列中执行。
- watch 的 flush 设置为 post
- 生命周期
mounted、updated
js
const queuePostRenderEffect: (
fn: SchedulerJobs,
suspense: SuspenseBoundary | null,
) => void = __FEATURE_SUSPENSE__
? __TEST__
? (fn, suspense) => queueEffectWithSuspense(fn, suspense)
: queueEffectWithSuspense
: queuePostFlushCb
挂载节点时,transition.enter、指令的 onMounted生命周期钩子

指令的 onUpdated 生命周期钩子、onUnmounted 生命周期钩子


组件的 onMounted 生命周期钩子、onUpdated 生命周期钩子


异步组件实例 update

异步组件 onUnmounted 生命周期钩子

queueEffectWithSuspense
根据当前是否有尚未完成的 Suspense 异步分支,来决定将回调立即放入全局后置队列,还是暂存到 Suspense 边界实例上,等待异步依赖完成后统一执行。
js
function queueEffectWithSuspense(
fn: Function | Function[],
suspense: SuspenseBoundary | null,
): void {
// 存在 Suspense 边界且有挂起分支
// 挂起状态:副作用暂时存储在 suspense.effects 中
if (suspense && suspense.pendingBranch) {
if (isArray(fn)) {
suspense.effects.push(...fn)
} else {
suspense.effects.push(fn)
}
} else {
// 将副作用添加到全局 post-flush 队列
queuePostFlushCb(fn)
}
}
queuePostFlushCb 加入后置任务队列
js
function queuePostFlushCb(cb: SchedulerJobs): void {
if (!isArray(cb)) {
if (activePostFlushCbs && cb.id === -1) {
// 这通常用于紧急任务,需要在当前执行队列中立即插入
activePostFlushCbs.splice(postFlushIndex + 1, 0, cb)
} else if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
// 避免重复添加同一个回调
pendingPostFlushCbs.push(cb)
cb.flags! |= SchedulerJobFlags.QUEUED
}
} else {
pendingPostFlushCbs.push(...cb)
}
// 触发刷新
queueFlush()
}
js
// 当前正在执行的后置任务队列
// 在执行后置任务时使用,避免直接修改 pendingPostFlushCbs
let activePostFlushCbs: SchedulerJob[] | null = null
js
// 存储待执行的后置任务
// DOM 更新后需要执行的任务,如 watchPostEffect、组件挂载后的回调等
const pendingPostFlushCbs: SchedulerJob[] = []
flushPostFlushCbs 执行后置任务
js
function flushPostFlushCbs(seen?: CountMap): void {
if (pendingPostFlushCbs.length) {
// 使用 Set 去除重复的任务,避免重复执行
// 根据任务的 ID 进行排序,确保任务按照正确的顺序执行
const deduped = [...new Set(pendingPostFlushCbs)].sort(
(a, b) => getId(a) - getId(b),
)
// 清空队列
pendingPostFlushCbs.length = 0
// #1947 already has active queue, nested flushPostFlushCbs call
// 如果是嵌套调用,将任务添加到当前活跃队列中,然后返回
if (activePostFlushCbs) {
activePostFlushCbs.push(...deduped)
return
}
// 设置活跃队列:将去重和排序后的任务队列设置为当前活跃队列
activePostFlushCbs = deduped
if (__DEV__) {
seen = seen || new Map()
}
// 遍历执行:按照顺序遍历执行每个任务
for (
postFlushIndex = 0;
postFlushIndex < activePostFlushCbs.length;
postFlushIndex++
) {
const cb = activePostFlushCbs[postFlushIndex]
if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
continue
}
if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
cb.flags! &= ~SchedulerJobFlags.QUEUED // 清除 QUEUED 标志
}
// 执行任务:如果任务没有被标记为 DISPOSED,执行任务
if (!(cb.flags! & SchedulerJobFlags.DISPOSED)) cb()
cb.flags! &= ~SchedulerJobFlags.QUEUED // 清除 QUEUED 标志
}
activePostFlushCbs = null // 重置活跃队列
postFlushIndex = 0 // 重置索引
}
}
在 render 中 执行

主任务执行完毕之后

nextTick
js
function nextTick<T, R>(
this: T,
fn?: (this: T) => R | Promise<R>,
): Promise<void | R> {
// 优先使用 currentFlushPromise(当前正在执行的刷新 Promise)
// 如果不存在,则使用 resolvedPromise(一个已解决的 Promise 实例)
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
currentFlushPromise 存在时,则在当前任务队列执行完毕后执行。
js
// 一个已解决的 Promise 实例
// 用于创建微任务,将任务延迟到下一个微任务执行
const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise<any>
Vue 的 nextTick 利用 Promise.then 在微任务中等待,但它只能保证DOM 已经更新 (即 Vue 完成 DOM 操作),并不能保证浏览器已经完成绘制。
如果需要在浏览器渲染后执行某操作(如获取元素的实际宽高),可以使用 requestAnimationFrame 或 setTimeout 延迟到下一帧。
生命周期


Javascript 事件循环
JavaScript 事件循环(Event Loop)模型任务:
- 同步任务(Synchronous) :立即执行,不经过事件循环的任务。
- 异步任务(Asynchronous) :不会立即执行,而是进入任务队列等待事件循环调度的任务。
- 宏任务(MacroTask) :具有最高优先级的异步任务,会在当前宏任务结束前、下一个宏任务开始前被清空。
- 微任务(MicroTask) :优先级较低的异步任务,每个宏任务执行完毕后会尝试执行微任务队列,然后再取下一个宏任务。
- 执行同步任务
- 遇到异步任务加入任务队列,同步执行完毕
- 执行微任务
- 期间会遇到微任务、异步任务则加入队列。
- 性能指标的
PerformanceObserver回调(微任务,优先于其他微任务执行) - 其他所有微任务 (
queueMicrotask、Promise.then、MutationObserver等),直到微任务队列(包含微任务执行期间新增的)为空
- 渲染阶段
- 执行所有 rAF 回调(渲染前专属阶段)
- 浏览器进行渲染操作(重排 / 重绘、合成图层、绘制到屏幕),这一步耗时由页面复杂度决定(目标是 16.6ms 内完成,对应 60Hz 刷新率)。
- 检查是否有空闲时间 :
- 如果当前帧总耗时(宏任务 + 微任务 + rAF + 渲染)<16.6ms,剩余时间就是 "空闲时段",执行
requestIdleCallback回调(可执行多个,直到时间用完或回调执行完)。 - 如果当前帧耗时已满 16.6ms(无空闲),则跳过,等待下一轮事件循环再检查。
- 如果当前帧总耗时(宏任务 + 微任务 + rAF + 渲染)<16.6ms,剩余时间就是 "空闲时段",执行
- 执行下一个宏任务(取一个宏任务)
- 执行
MessageChannel的onmessage回调(宏任务,优先于普通定时器) - 执行 IntersectionObserver 回调(宏任务)
- 执行其他普通宏任务(
setTimeout、setInterval等)
- 执行
事件循环的每一轮,宏任务最多只执行一个,然后清空微任务队列,再取下一个宏任务。微任务可能产生更多微任务,会一次性全部执行完,因此要防止死循环。