快速浏览
Vue 的调度器(Scheduler)主要负责管理和执行异步任务队列。
- 核心数据结构:
const queue: SchedulerJob[] = [] // 主任务队列
const pendingPostFlushCbs: SchedulerJob[] = [] // 后置任务队列
export enum SchedulerJobFlags {
QUEUED = 1 << 0, // 值为 1,表示任务已经被加入队列
PRE = 1 << 1, // 值为 2,表示这是一个前置任务
ALLOW_RECURSE = 1 << 2, // 值为 4,允许任务递归触发自身
DISPOSED = 1 << 3, // 值为 8,表示任务已被销毁
}
- 主要方法:
- nextTick:
export function nextTick(fn) {
// 返回一个 Promise,用于在下一个微任务执行回调
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(fn) : p
}
- queueJob:
export function queueJob(job: SchedulerJob) {
// 将任务添加到主队列中
// 使用二分查找确保任务按 id 排序
// 设置任务的 QUEUED 标志
// 触发 queueFlush
- queuePostFlushCb:
export function queuePostFlushCb(cb: SchedulerJobs): void {
// 用于处理需要在 DOM 更新后执行的回调函数
if (!isArray(cb)) {
// 单个回调的处理
if (activePostFlushCbs && cb.id === -1) {
// 如果当前正在执行后置刷新回调,且传入的回调 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)
}
}
- flushPreFlushCbs:
export function flushPreFlushCbs(
instance?: ComponentInternalInstance, // 组件实例
seen?: CountMap, // 用于开发环境下检测递归更新
i: number = flushIndex + 1 // 开始处理的索引位置,跳过当前的任务
): void {
// 主要用于执行预刷新(pre-flush)回调函数,这是 Vue 调度系统的一个重要部分
// 注意,在 3.5 之前的版本,预刷新回调函数只有 watch api 能触发。
if (__DEV__) {
seen = seen || new Map()
}
// 遍历队列中的回调函数
for (; i < queue.length; i++) {
const cb = queue[i]
// 检查是否是预刷新回调(PRE flag)
if (cb && cb.flags! & SchedulerJobFlags.PRE) {
// 如果指定了组件实例,则跳过不属于该实例的回调
if (instance && cb.id !== instance.uid) {
continue
}
// 从队列中移除当前回调
queue.splice(i, 1)
i--
// 执行回调前的标志位处理
if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
cb.flags! &= ~SchedulerJobFlags.QUEUED
}
// 执行回调
cb()
// 执行后的标志位处理
if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
cb.flags! &= ~SchedulerJobFlags.QUEUED
}
}
}
}
- queueJobFlushCb:
export function queuePostFlushCb(cb: SchedulerJob) {
// 添加后置回调任务
// 支持单个任务或任务数组
}
- flushJobs:
function flushJobs(seen?: CountMap) {
// 核心任务执行函数
// 1. 执行主队列中的任务
// 2. 执行后置任务
// 3. 处理递归更新警察
// 4. 错误处理
}
- 重要特性:
- 任务去重:通过 flags 标记避免重复添加任务
- 任务排序:使用二分查找确保父组件在子组件之前更新【当父组件更新事件导致子组件被卸载,那么子组件的更新将被忽略】
- 递归限制:通过 RECURSION_LIMIT 限制递归更新次数
- 错误处理:使用 callWithErrorHandling 包装任务执行
-
调度器工作流程:
- 组件更新时通过 queueJob 添加任务
- 任务进入队列并触发 queueFlush
- 在下一个微任务队列中执行 flushJobs
- 按顺序执行队列中的任务
- 执行后置任务
- 处理新产生的任务直到队列清空
工作机制
- 核心数据结构:
const queue: SchedulerJob[] = [] // 主任务队列
const pendingPostFlushCbs: SchedulerJob[] = [] // 后置任务队列
- 主要工作流程:
- 任务入队
// 通过 queueJob 将任务加入主队列
export function queueJob(job: SchedulerJob) {
// 确保任务不重复入队
if(!(job.flags! & SchedulerJobFlags.QUEUED) {
if (
!lastJob ||
// fast path when the job id is larger than the tail
(!(job.flags! & SchedulerJobFlags.PRE) && jobId >= getId(lastJob))
) {
queue.push(job)
} else {
// 使用二分查找确定插入位置,保证父组件更新在子组件之前
queue.splice(findInsertionIndex(jobId), 0, job)
}
// 触发刷新
queueFlush()
}
}
- 刷新机制
function queueFlush() {
// 使用 Promise 确保异步执行
if (!currentFlushPromise) {
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
- 执行顺序:
- 预刷新回调(Pre Flush):
- 通过 flushPreFlushCbs 执行
- 主要用于组件更新前的准备工作
- 主队列任务:
- 通过 flushJobs 执行
- 按照组件层级顺序处理更新
- 后置刷新回调(Post Flush):
- 通过 flushPostFlushCbs 执行
- 用于组件更新后的清理工作
- 预刷新回调(Pre Flush):
- 重要特性:
- 递归控制:
const RECURSION_LIMIT = 100 // 通过 checkRecusiveUpdates 防止无限递归- 任务去重:
// 后置回调去重 const deduped = [...new Set(pendingPostFlushCbs)]- 异步调度:
export function next(fn?) { // 返回 Promise,确保任务在下一个微任务队列执行 return fn ? p.then(fn) : p } - 核心价值:
- 性能优化:合并多次更新,避免重复渲染
- 执行顺序:确保父组件先于子组件更新
- 异步处理:避免阻塞主线程
- 安全性:防止递归溢出,处理错误边界
watch 与调度器的关系
-
任务调度:
- watch 创建的副作用会被包装成一个 job
- 当数据变化时,这个 job 会被添加到调度器的队列中
- 调度器负责统一管理和执行这些任务
-
常规调度流程:
// 1. 触发数据变化
// 2. 调度器将 job 加入队列
queueJob(job)
// 3. 在下一个微任务中执行
nextTick(() => {
flushJobs()
}
- Pre-Queue 的使用场景:
3.1 组件更新前的 watch:
// 通过设置 flush: 'pre' 创建前置监听器
watch(source, callback, {
flush: 'pre' // 在组件更新前执行
}
3.2 特点
- 在组件更新前执行
- 可以在 DOM 更新前访问旧的 DOM 状态
- 适合需要在组件更新前进行数据准备的场景
3.3 实现机制:
// 通过设置 SchedulerJobFlags.PRE 标识
if (flush == 'pre') {
job.flags! |= SchedulerJobFlags.PRE
}
- Post-Queue 的使用场景
4.1 组件更新后的 watch:
// 通过设置 flush: 'post' 创建后置监听器
watch(source, callback, {
flush: 'post' // 在组件更新后执行
}
4.2 特点:
- 在组件更新完成后执行
- 可以访问更新后的 DOM
- 适合需要基于更新后的 DOM 进行操作的场景
4.3 实现机制:
// 使用 queuePostFlushCb 添加到后置队列
if (flush === 'post') {
queuePostFlushCb(job)
}
- 调度顺序:
完整的调度顺序如下:
pre-queue 任务
组件更新
normal-queue 任务
post-queue 任务
6. 具体实现实例
// 前置监听器
watch(
() => state.value,
() => {
// 在组件更新前执行
console.log('pre watcher triggered')
},
{ flush: 'pre' }
)
// 后置监听器
watch(
() => state.value,
() => {
// 在组件更新后执行
console.log('post watcher triggered')
},
{ flush: 'post' }
)
// 同步监听器(默认)
watch(
() => state.value,
() => {
// 同步执行,但会被放入微任务队列
console.log('sync watcher triggered')
}
)
- 调度器中的处理
// 处理前置队列
export function flushPreFlushCbs() {
for (let i = 0; i < queue.length; i++) {
const cb = queue[i]
if (cb && cb.flags! & SchedulerJobFlags.PRE) {
queue.splice(i, 1)
i--
cb()
}
}
}
// 处理后置队列
export function flushPostFlushCbs() {
if (pendingPostFlushCbs.length) {
const deduped = [...new Set(pendingPostFlushCbs)]
pendingPostFlushCbs.length = 0
for (let i = 0; i < deduped.length; i++) {
deduped[i]()
}
}
}