vue3.x 调度器(Scheduler)实现机制

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)
  }
}
  • resolvedPromisePromise.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
  • 生命周期 mountedupdated
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 操作),并不能保证浏览器已经完成绘制

如果需要在浏览器渲染后执行某操作(如获取元素的实际宽高),可以使用 requestAnimationFramesetTimeout 延迟到下一帧。

生命周期

Javascript 事件循环

JavaScript 事件循环(Event Loop)模型任务:

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

事件循环的每一轮,宏任务最多只执行一个,然后清空微任务队列,再取下一个宏任务。微任务可能产生更多微任务,会一次性全部执行完,因此要防止死循环。

相关推荐
MPGWJPMTJT1 小时前
从 Volta 迁移到 mise:Windows 下 Node 版本管理切换记录
前端·node.js
米饭同学i1 小时前
Vue2 + Webpack 老项目启动报错
前端
小彭努力中1 小时前
205.Vue3 + OpenLayers:加载动画,采用 CSS 的 @keyframes 方式
前端·css·vue.js·openlayers·cesium·webgis
木斯佳1 小时前
前端八股文面经大全:上海威派格前端实习(2026-05-07)·面经深度解析
前端
_Twink1e1 小时前
基于Vue的纯前端的库存销售系统
前端·vue.js·vue·web
幽络源小助理1 小时前
音频在线剪切助手网页版源码 – 纯前端HTML单文件免费分享
前端·音视频
陈振wx:zchen20081 小时前
前端-面试题-Vue
前端·vue.js
计算机安禾1 小时前
【c++面向对象编程】第5篇:类与对象(四):赋值运算符重载
java·前端·c++
Moment1 小时前
从 beginWork 到 completeWork,Fiber 树是怎么“盖”出来的❓❓❓
前端·javascript·面试