Skip to content

实现 nextTick 与异步调度

本节对标 Vue 3 源码 @vue/runtime-core 中的 scheduler.ts

为什么需要异步调度?

考虑以下场景:

ts
const count = ref(0)

count.value = 1
count.value = 2
count.value = 3

如果每次赋值都同步触发组件重新渲染,那么 DOM 会更新 3 次。但实际上只需要在最后更新一次(值为 3)即可。

Vue 3 的解决方案:将组件更新 effect 放入异步队列,在当前同步代码执行完毕后(微任务阶段),统一执行一次更新。

同步代码                      微任务阶段
count.value = 1  → queueJob  ─┐
count.value = 2  → queueJob   │(去重,同一 job 只入队一次)
count.value = 3  → queueJob  ─┘
                                → flushJobs → componentUpdateFn(只执行一次)

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

nextTick 的实现

nextTick 是整个调度系统的基石——利用 Promise.resolve() 将回调推入微任务队列:

ts
// packages/runtime-core/src/scheduler.ts

// 预创建一个已 resolve 的 Promise,作为微任务调度的基础,避免重复创建 Promise 实例
const resolvedPromise = Promise.resolve()
// 记录当前正在执行的 flush 任务对应的 Promise
// 如果有正在 flush 的任务,nextTick 会等待该轮 flush 完成后再执行回调
let currentFlushPromise: Promise<void> | null = null

// nextTick:将回调推入微任务队列,确保在当前 flush 完成后执行
// 如果没有传入 fn,则返回 Promise 供 await 使用
export function nextTick(fn?: () => void): Promise<void> {
  // 优先使用 currentFlushPromise,保证回调在当前 flush 完成后才执行
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(fn) : p // 有回调则链式调用,否则直接返回 Promise
}

设计分析

  1. resolvedPromise:预创建的已 resolve 的 Promise,避免重复创建
  2. currentFlushPromise:如果当前有正在 flush 的任务,nextTick 会等待该轮 flush 完成后再执行。这确保了 nextTick 回调中能获取到最新的 DOM 状态
  3. 返回 Promise:支持 await nextTick() 的用法

为什么是微任务而不是宏任务?

同步代码 → 微任务(Promise.then / MutationObserver)→ 渲染 → 宏任务(setTimeout)

使用微任务可以在浏览器渲染之前完成所有状态更新和 DOM 操作,避免用户看到中间状态的闪烁。Vue 2 曾在微任务和宏任务之间反复切换,Vue 3 最终统一使用 Promise.resolve().then() 微任务。

调度队列设计

核心数据结构

ts
// packages/runtime-core/src/scheduler.ts

// 调度任务接口,扩展了 Function 类型,添加调度相关的元信息
export interface SchedulerJob extends Function {
  id?: number                              // 任务 ID,用于排序(父组件 id < 子组件 id)
  active?: boolean                         // 是否激活,false 时跳过执行
  computed?: boolean                       // 是否为计算属性的 effect
  allowRecurse?: boolean                   // 是否允许递归(job 执行期间再次入队自身)
  ownerInstance?: ComponentInternalInstance // 所属的组件实例
}

const queue: SchedulerJob[] = []               // 主队列:存放组件更新 effect
const pendingPostFlushCbs: SchedulerJob[] = [] // 后置队列:存放 mounted/updated 等 hooks
let isFlushing = false    // 标记是否正在执行队列中的任务
let isFlushPending = false // 标记是否已注册微任务等待 flush
let flushIndex = 0         // 当前正在执行的主队列任务索引

Vue 3 的调度系统有三类队列:

Pre Flush Queue     →    Main Queue (queue)    →    Post Flush Queue
(queuePreFlushCb)        (queueJob)                 (queuePostFlushCb)
                                                     
watchers (pre)           组件更新 effect              mounted/updated hooks
                                                     watchers (post)

queueJob —— 主队列入队

ts
// 将任务加入主队列,核心逻辑是去重——同一个 job 不会重复入队
export function queueJob(job: SchedulerJob) {
  // 去重:同一个 job 不会重复入队
  // 使用 includes 检查时要考虑 flushIndex,避免跳过正在处理中的 job
  // 如果 allowRecurse 为 true,从 flushIndex + 1 开始检查,允许当前 job 再次入队
  if (
    !queue.length ||
    !queue.includes(job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)
  ) {
    if (job.id == null) {
      queue.push(job) // 没有 id 的 job 直接推入队列末尾
    } else {
      // 有 id 的 job 按 id 插入到正确位置(保持排序)
      // 这样可以确保父组件(小 id)在子组件(大 id)之前执行
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    queueFlush() // 触发异步 flush(如果尚未触发)
  }
}

去重机制是异步调度的关键:

ts
const count = ref(0)

// 假设组件的更新 effect 是 job A
count.value = 1  // trigger → scheduler → queueJob(A)  → queue: [A]
count.value = 2  // trigger → scheduler → queueJob(A)  → queue: [A](A 已在队列中,跳过)
count.value = 3  // trigger → scheduler → queueJob(A)  → queue: [A](A 已在队列中,跳过)

// 微任务阶段
// flushJobs → 执行 A → 组件只更新一次,count.value 已经是 3

queueFlush —— 触发异步 flush

ts
// 触发异步 flush:通过 Promise.then 将 flushJobs 注册为微任务
function queueFlush() {
  // 双重标志位保护:只有在既没有正在 flush、也没有等待 flush 时才注册微任务
  // 这样即使 queueJob 被调用多次,也只会注册一次微任务
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true // 标记已注册等待 flush
    // 将 flushJobs 放入微任务队列,等同步代码全部执行完后再统一 flush
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

通过 isFlushPending 标志位确保只注册一次微任务,即使 queueJob 被调用多次。

flushJobs —— 执行队列

ts
// 执行队列中的所有任务——调度系统的核心执行函数
function flushJobs() {
  isFlushPending = false // 微任务已开始执行,清除等待标志
  isFlushing = true      // 标记正在 flush,防止重复触发

  // 排序:按 job.id 升序
  // 组件的 id 在创建时递增分配,父组件 id < 子组件 id
  // 这保证了父组件先于子组件更新
  queue.sort(comparator)

  try {
    // 遍历执行主队列中的所有任务
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      // 只执行存在且处于激活状态的 job
      if (job && job.active !== false) {
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) // 带错误处理地执行 job
      }
    }
  } finally {
    // 无论是否出错,都要清理状态
    flushIndex = 0      // 重置索引
    queue.length = 0    // 清空主队列

    // 执行 post flush 回调(如 mounted、updated 等生命周期 hooks)
    flushPostFlushCbs()

    isFlushing = false          // 标记 flush 完成
    currentFlushPromise = null  // 清除当前 flush 的 Promise 引用

    // 如果在 flush 过程中有新的 job 被加入(如 hook 中触发了新的状态变更),递归 flush
    if (queue.length || pendingPostFlushCbs.length) {
      flushJobs()
    }
  }
}

// 比较器:按 job.id 升序排列,确保父组件先于子组件更新
const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
  const diff = getId(a) - getId(b)
  return diff
}

// 获取 job 的 id,没有 id 的 job 排在最后(Infinity)
const getId = (job: SchedulerJob): number =>
  job.id == null ? Infinity : job.id

排序的重要性

Parent (id: 1)
  └── Child (id: 2)
       └── GrandChild (id: 3)

如果 Child 和 Parent 都需要更新,排序保证 Parent 先更新。这很重要,因为:

  1. 父组件更新可能导致子组件被卸载(条件渲染),此时子组件的更新就是多余的
  2. 父组件的 props 变化需要先传递给子组件,子组件再基于新 props 更新

Pre Flush 队列

Pre flush 回调在主队列执行前运行,主要用于 watchflush: 'pre' 模式(默认模式):

ts
const pendingPreFlushCbs: SchedulerJob[] = []       // 待执行的 pre flush 回调列表
let activePreFlushCbs: SchedulerJob[] | null = null  // 正在执行中的 pre flush 回调快照
let preFlushIndex = 0                                // 当前执行的 pre flush 回调索引

// 将回调加入 pre flush 队列(用于 watch 的 flush: 'pre' 模式)
export function queuePreFlushCb(cb: SchedulerJob) {
  // 去重:同一个回调不会重复入队
  if (!pendingPreFlushCbs.includes(cb)) {
    pendingPreFlushCbs.push(cb)
    queueFlush() // 确保已注册微任务
  }
}

// 执行所有 pre flush 回调
function flushPreFlushCbs() {
  if (pendingPreFlushCbs.length) {
    // 通过 Set 去重后生成快照,避免在执行过程中数组被修改导致问题
    activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
    pendingPreFlushCbs.length = 0 // 清空待执行列表

    // 遍历执行所有 pre flush 回调
    for (preFlushIndex = 0; preFlushIndex < activePreFlushCbs.length; preFlushIndex++) {
      activePreFlushCbs[preFlushIndex]()
    }
    activePreFlushCbs = null // 清除快照引用
    preFlushIndex = 0

    // 递归调用:pre flush 中可能产生新的 pre flush cb(如 watcher 回调中又触发了其他 watcher)
    flushPreFlushCbs()
  }
}

Post Flush 队列

Post flush 回调在主队列执行后运行,用于生命周期 hooks(mountedupdated)和 watchflush: 'post' 模式:

ts
const pendingPostFlushCbs: SchedulerJob[] = []       // 待执行的 post flush 回调列表
let activePostFlushCbs: SchedulerJob[] | null = null  // 正在执行中的 post flush 回调快照
let postFlushIndex = 0                                // 当前执行的 post flush 回调索引

// 将回调加入 post flush 队列(用于生命周期 hooks 和 watch 的 flush: 'post' 模式)
export function queuePostFlushCb(cb: SchedulerJob | SchedulerJob[]) {
  if (!Array.isArray(cb)) {
    // 单个回调:检查是否已在活跃队列中,避免重复执行
    if (
      !activePostFlushCbs ||
      !activePostFlushCbs.includes(cb, cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex)
    ) {
      pendingPostFlushCbs.push(cb)
    }
  } else {
    // 如果传入数组(如 invokeArrayFns 的 hooks 数组),直接展开推入
    // 数组中的 hook 已经在组件级别去重,这里不需要再检查
    pendingPostFlushCbs.push(...cb)
  }
  queueFlush() // 确保已注册微任务
}

// 执行所有 post flush 回调
function flushPostFlushCbs() {
  if (pendingPostFlushCbs.length) {
    // 通过 Set 去重,避免同一个回调在一轮 flush 中被执行多次
    const deduped = [...new Set(pendingPostFlushCbs)]
    pendingPostFlushCbs.length = 0 // 清空待执行列表

    // 如果已经有活跃的 post flush 队列在执行,将新的回调追加到其中
    // 这处理了 post flush 回调中又产生新的 post flush 回调的场景
    if (activePostFlushCbs) {
      activePostFlushCbs.push(...deduped)
      return
    }

    activePostFlushCbs = deduped
    // 按 id 排序,保证父组件的 hooks 先于子组件执行
    activePostFlushCbs.sort((a, b) => getId(a) - getId(b))

    // 遍历执行所有 post flush 回调
    for (postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++) {
      activePostFlushCbs[postFlushIndex]()
    }

    activePostFlushCbs = null // 清除快照引用
    postFlushIndex = 0
  }
}

完整的 flush 流程

ts
// 完整的 flushJobs 流程——按 pre → main → post 三级队列依次执行
function flushJobs() {
  isFlushPending = false // 清除等待标志
  isFlushing = true      // 标记正在执行

  // 1. 执行 pre flush 回调(watchers with flush: 'pre')
  // 在主队列之前执行,确保 watcher 的回调能在组件更新前运行
  flushPreFlushCbs()

  // 2. 排序主队列
  // 按 id 升序排列,保证父组件(小 id)先于子组件(大 id)更新
  queue.sort(comparator)

  // 3. 执行主队列(组件更新)
  try {
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job && job.active !== false) {
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    flushIndex = 0      // 重置索引
    queue.length = 0    // 清空主队列

    // 4. 执行 post flush 回调(lifecycle hooks, watchers with flush: 'post')
    // 此时所有组件的 DOM 更新已完成,可以安全访问最新的 DOM
    flushPostFlushCbs()

    isFlushing = false          // 标记 flush 完成
    currentFlushPromise = null  // 清除 Promise 引用

    // 5. 如果过程中产生了新任务(如 hook 中触发了新的状态变更),再次 flush
    // 递归调用确保所有任务都被处理完毕
    if (queue.length || pendingPostFlushCbs.length) {
      flushJobs()
    }
  }
}

执行时序图

同步代码阶段:
  count.value = 1       → queueJob(componentUpdate)
  count.value = 2       → queueJob(componentUpdate)  [去重,不入队]
  list.value.push(item) → queueJob(componentUpdate)  [去重,不入队]

                         ↓ 同步代码执行完毕

微任务阶段 (Promise.then):
  flushJobs()

    ├── flushPreFlushCbs()     ← watch(source, cb)  默认 flush: 'pre'

    ├── queue.sort()           ← 父 → 子排序

    ├── for job of queue:      ← 组件 componentUpdateFn
    │     ├── beforeUpdate hooks(同步)
    │     ├── render()
    │     ├── patch()
    │     └── queuePostFlushCb(updated hooks)

    ├── flushPostFlushCbs()    ← mounted/updated hooks
    │                          ← watch(source, cb, { flush: 'post' })

    └── 如果有新任务 → 递归 flushJobs()

                         ↓ 微任务完成

浏览器渲染(painting)

调度器如何与组件更新连接

setupRenderEffect 中,组件的更新 effect 使用 scheduler 来触发异步更新:

ts
// packages/runtime-core/src/renderer.ts
function setupRenderEffect(instance, initialVNode, container, anchor) {
  // 定义组件的更新函数,包含挂载和更新两个分支
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      // mount 逻辑...
    } else {
      // update 逻辑...
    }
  }

  // 创建响应式副作用
  // 第二个参数是 scheduler(调度器),当响应式数据变化时不会直接执行 effect
  // 而是调用 scheduler 将更新任务入队,实现异步批量更新
  const effect = new ReactiveEffect(
    componentUpdateFn,
    // scheduler: 响应式数据变化时不直接执行 effect,而是入队
    () => queueJob(update),
  )

  const update: SchedulerJob = (instance.update = () => effect.run())
  update.id = instance.uid  // 设置 job 的 id 为组件的 uid,用于排序(父→子)
  update() // 首次调用,触发挂载流程
}

关键点ReactiveEffect 的第二个参数是 scheduler。当响应式数据变化触发 trigger 时,如果 effect 有 scheduler,就调用 scheduler 而不是直接调用 effect.run()

ts
// packages/reactivity/src/effect.ts

// 触发单个 effect:根据是否有 scheduler 决定执行方式
function triggerEffect(effect: ReactiveEffect) {
  if (effect.scheduler) {
    // 如果 effect 有调度器,调用调度器而非直接执行
    // 对于组件更新 effect,scheduler 就是 () => queueJob(update)
    effect.scheduler()  // → queueJob(update)
  } else {
    // 没有调度器的 effect 直接同步执行(如普通的 watchEffect)
    effect.run()
  }
}

简化实现

ts
// scheduler.ts

const queue: Function[] = []             // 主队列:存放组件更新任务
const postFlushCbs: Function[] = []      // 后置队列:存放生命周期 hooks 等回调
const resolvedPromise = Promise.resolve() // 预创建的已 resolve 的 Promise
let currentFlushPromise: Promise<void> | null = null // 当前 flush 任务的 Promise
let isFlushing = false    // 是否正在执行队列
let isFlushPending = false // 是否已注册微任务等待执行

// nextTick:返回一个在当前 flush 完成后 resolve 的 Promise
export function nextTick(fn?: () => void): Promise<void> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(fn) : p
}

// 将任务加入主队列,自动去重
export function queueJob(job: any) {
  if (!queue.includes(job)) {
    queue.push(job)
    queueFlush() // 确保触发异步 flush
  }
}

// 将回调加入后置队列(支持单个或数组)
export function queuePostFlushCb(cb: Function | Function[]) {
  if (Array.isArray(cb)) {
    postFlushCbs.push(...cb) // 数组直接展开推入
  } else {
    postFlushCbs.push(cb)
  }
  queueFlush()
}

// 触发异步 flush:利用 Promise.then 注册微任务
function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs) // 将 flushJobs 放入微任务队列
  }
}

// 核心执行函数:排序后依次执行所有任务
function flushJobs() {
  isFlushPending = false
  isFlushing = true

  // 按 id 排序,确保父组件先于子组件更新;无 id 的任务排在最后
  queue.sort((a, b) => (a.id ?? Infinity) - (b.id ?? Infinity))

  try {
    // 遍历执行主队列中的每个任务
    for (let i = 0; i < queue.length; i++) {
      const job = queue[i]
      if (job) {
        job() // 执行组件的 componentUpdateFn
      }
    }
  } finally {
    queue.length = 0          // 清空主队列
    flushPostFlushCbs()       // 执行后置回调(mounted/updated hooks)
    isFlushing = false
    currentFlushPromise = null

    // 如果 flush 过程中又产生了新的任务,递归执行
    if (queue.length || postFlushCbs.length) {
      flushJobs()
    }
  }
}

// 执行后置队列中的所有回调
function flushPostFlushCbs() {
  if (postFlushCbs.length) {
    // 通过 Set 去重,避免同一回调被重复执行
    const cbs = [...new Set(postFlushCbs)]
    postFlushCbs.length = 0 // 清空待执行列表
    for (let i = 0; i < cbs.length; i++) {
      cbs[i]() // 依次执行每个回调
    }
  }
}

对比 React 调度器(Scheduler)

维度Vue 3 SchedulerReact Scheduler
调度粒度组件级(每个组件一个 job)Fiber 级(每个 Fiber 一个 work unit)
优先级无优先级,按父→子顺序5 个优先级(Immediate / UserBlocking / Normal / Low / Idle)
时间切片无(同步执行所有 jobs)有(5ms 时间切片,可中断)
异步机制Promise.resolve().then() 微任务MessageChannel 宏任务
可中断不可中断(一旦开始 flush,同步执行完)可中断(通过 shouldYield 检查)
批量更新自动(同一微任务内的变更自动合并)React 18+ 自动批量(createRoot
并发模式有(Concurrent Mode,可中断渲染)
复杂度~200 行代码~600 行代码

核心差异:同步 vs 可中断

Vue 3:
  queueJob → flushJobs → 同步执行所有组件更新 → 完成
  (简单可预测,但大量组件更新时可能阻塞主线程)

React:
  scheduleCallback(priority, work) → workLoop → 执行一个 fiber
    → shouldYield()? → 是 → 让出主线程 → requestIdleCallback/MessageChannel → 继续
                      → 否 → 执行下一个 fiber
  (复杂但不阻塞主线程,保持页面响应性)

为什么 Vue 3 不需要时间切片?

Vue 3 的响应式系统可以精确知道哪些组件需要更新(依赖收集),不像 React 需要从根节点开始 reconcile。因此 Vue 3 的更新范围通常更小,同步执行也不会造成明显的卡顿。

测试用例

ts
import { describe, it, expect, vi } from 'vitest'
import { ref, nextTick, h, render } from '../src'

describe('scheduler', () => {
  // 测试:nextTick 的回调在同步代码执行完毕后才执行
  it('nextTick', async () => {
    const calls: number[] = []

    calls.push(1)             // 同步:先推入 1
    const p = nextTick(() => {
      calls.push(3)           // 微任务:最后推入 3
    })
    calls.push(2)             // 同步:再推入 2

    await p
    // 断言执行顺序:同步代码(1, 2)先于微任务(3)
    expect(calls).toEqual([1, 2, 3])
  })

  // 测试:nextTick 支持 await 用法
  it('nextTick with await', async () => {
    let value = 0
    value = 1
    await nextTick() // 等待微任务完成
    expect(value).toBe(1)
  })

  // 测试:多次修改响应式数据只触发一次渲染(批量更新)
  it('should batch multiple state changes', async () => {
    const renderSpy = vi.fn() // 用于追踪渲染函数被调用的次数
    const count = ref(0)

    const Comp = {
      setup() {
        return () => {
          renderSpy() // 每次渲染都会调用
          return h('div', null, String(count.value))
        }
      },
    }

    const root = document.createElement('div')
    render(h(Comp), root)
    // 断言:首次挂载渲染了一次
    expect(renderSpy).toHaveBeenCalledTimes(1)

    // 连续修改 3 次——由于异步调度 + 去重,不会立即触发渲染
    count.value++
    count.value++
    count.value++

    // 断言:同步代码中修改后,渲染函数还没有被再次调用
    expect(renderSpy).toHaveBeenCalledTimes(1)

    await nextTick() // 等待异步调度完成
    // 断言:3 次修改只触发了 1 次额外渲染(总共 2 次)
    expect(renderSpy).toHaveBeenCalledTimes(2)
    // 断言:DOM 显示最终值 3
    expect(root.innerHTML).toBe('<div>3</div>')
  })

  // 测试:父组件在子组件之前更新(按 id 排序)
  it('should update parent before child', async () => {
    const calls: string[] = [] // 记录渲染顺序

    const Child = {
      props: ['msg'],
      setup(props: any) {
        return () => {
          calls.push('child render') // 记录子组件渲染
          return h('span', null, props.msg)
        }
      },
    }

    const msg = ref('hello')
    const Parent = {
      setup() {
        return () => {
          calls.push('parent render') // 记录父组件渲染
          return h('div', null, [h(Child, { msg: msg.value })])
        }
      },
    }

    const root = document.createElement('div')
    render(h(Parent), root)
    calls.length = 0 // 清空首次渲染的记录

    msg.value = 'world' // 修改数据触发更新
    await nextTick()

    // 断言:父组件先于子组件渲染,体现了调度器的排序机制
    expect(calls[0]).toBe('parent render')
  })

  // 测试:同一个 job 不会被重复执行(去重机制)
  it('should deduplicate jobs', async () => {
    const spy = vi.fn()
    const count = ref(0)

    const Comp = {
      setup() {
        return () => {
          spy() // 追踪渲染次数
          return h('div', null, String(count.value))
        }
      },
    }

    const root = document.createElement('div')
    render(h(Comp), root)
    expect(spy).toHaveBeenCalledTimes(1) // 首次渲染

    // 修改 100 次——queueJob 去重后只保留一个 job
    for (let i = 0; i < 100; i++) {
      count.value = i
    }

    await nextTick()
    // 断言:100 次修改只触发了 1 次额外渲染(总共 2 次)
    expect(spy).toHaveBeenCalledTimes(2)
  })

  // 测试:nextTick 回调中可以获取到更新后的 DOM
  it('nextTick should see updated DOM', async () => {
    const count = ref(0)

    const Comp = {
      setup() {
        return () => h('div', null, String(count.value))
      },
    }

    const root = document.createElement('div')
    render(h(Comp), root)
    expect(root.innerHTML).toBe('<div>0</div>')

    count.value = 42 // 修改数据

    await nextTick() // 等待 DOM 更新完成
    // 断言:nextTick 后 DOM 已经是最新的值
    expect(root.innerHTML).toBe('<div>42</div>')
  })

  // 测试:post flush 回调在主队列(render)执行后才运行
  it('queuePostFlushCb should run after main queue', async () => {
    const calls: string[] = []

    const count = ref(0)
    const Comp = {
      setup() {
        return () => {
          calls.push('render') // 记录渲染时机
          return h('div', null, String(count.value))
        }
      },
    }

    const root = document.createElement('div')
    render(h(Comp), root)
    calls.length = 0 // 清空首次渲染记录

    count.value++ // 触发更新

    await nextTick()
    // 断言:render 在 post flush 之前执行
    expect(calls[0]).toBe('render')
  })
})

本节小结

  1. nextTick —— 基于 Promise.resolve().then() 的微任务调度,确保回调在 DOM 更新后执行
  2. queueJob —— 主队列入队,自动去重,同一组件的更新 effect 只执行一次
  3. flushJobs —— 按 job.id 排序后依次执行,保证父→子的更新顺序
  4. 三级队列 —— pre flush(watchers)→ main queue(组件更新)→ post flush(生命周期 hooks)
  5. 与 React 的差异 —— Vue 3 使用微任务同步执行,简单可预测;React 使用宏任务 + 时间切片,支持可中断的并发渲染

下一节实现 Teleport 与 KeepAlive 内置组件。

用心学习,用代码说话 💻