首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >源码阅读入门:解析 Vue3 的 nextTick 原理(附简化版实现)

源码阅读入门:解析 Vue3 的 nextTick 原理(附简化版实现)

作者头像
fruge365
发布2025-12-15 13:47:17
发布2025-12-15 13:47:17
280
举报

源码阅读入门:解析 Vue3 的 nextTick 原理(附简化版实现)

目标:从调度器视角理解 Vue3 的 nextTick,掌握其与微任务、更新队列的关系,并提供一个可运行的简化版实现。

背景与现象

  • 组件状态更新并不立即反映到 DOM,Vue 会合并多次变更后统一刷新视图
  • nextTick 用于等待本轮视图刷新完成,再执行回调或继续代码
  • 常见用法:await nextTick() 后读取 DOM 或进行尺寸计算

原理概览

  • Vue3 将更新任务收集到队列,统一在微任务中批量执行
  • 微任务优先级高于宏任务,可确保在同一事件循环中尽快刷新
  • nextTick 返回当前一次队列刷新的 Promise,实现“等这一次刷新完成再继续”

核心机制(概念)

  • 任务队列:收集组件渲染、副作用等任务,避免重复执行
  • 刷新调度:使用已解析的 Promise 安排一个微任务,触发队列处理
  • 刷新状态:isFlushingisFlushPending 防止重复调度
  • 执行顺序:按任务 id 排序,保证父子、组件层级的正确更新顺序

简化版实现(可运行)

调度器与 nextTick
代码语言:javascript
复制
type Job = (() => void) & { id?: number }

const queue: Job[] = []
let isFlushing = false
let isFlushPending = false
let currentFlushPromise: Promise<void> | null = null
const resolved = Promise.resolve()

export function queueJob(job: Job) {
  if (!queue.includes(job)) {
    queue.push(job)
  }
  queueFlush()
}

function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolved.then(flushJobs)
  }
}

function flushJobs() {
  isFlushing = true
  isFlushPending = false
  queue.sort((a, b) => (getId(a) - getId(b)))
  for (let i = 0; i < queue.length; i++) {
    const job = queue[i]
    job()
  }
  queue.length = 0
  isFlushing = false
  currentFlushPromise = null
}

function getId(fn: Job) { return fn.id ?? 0 }

export function nextTick<T = void>(fn?: () => T | Promise<T>) {
  const p = currentFlushPromise ?? resolved
  return fn ? p.then(fn) : p
}
使用与验证
代码语言:javascript
复制
const jobA: Job = Object.assign(() => { console.log('A') }, { id: 1 })
const jobB: Job = Object.assign(() => { console.log('B') }, { id: 2 })

queueJob(jobB)
queueJob(jobA)

await nextTick()

Vue3 中的关系链路(简析)

  • 触发渲染:响应式变更触发组件副作用,副作用被 queueJob 入队
  • 安排微任务:currentFlushPromise = resolved.then(flushJobs) 提交一次刷新
  • 用户等待:nextTick 返回 currentFlushPromiseresolved,保证回调在刷新之后执行
  • 刷新完成:flushJobs 执行、清空队列、重置状态,currentFlushPromise 设为 null

关键特性与边界

  • 任务去重:同一任务在一次刷新中只执行一次
  • 排序执行:通过 id 排序保证父组件先于子组件,避免不一致
  • 微任务时机:在事件回调结束后但在宏任务前刷新视图
  • 多次变更合并:同一 tick 内的多次状态更新被合并处理

对比常见替代方案

  • setTimeout(fn) 属于宏任务,会延后到下一轮事件循环,性能与时机不理想
  • Promise.resolve().then(fn) 是微任务,满足刷新后的最早可执行时机
  • 旧环境可考虑 MutationObserverMessageChannel 作为微任务降级方案

进一步扩展(贴近真实)

  • 增加 prepost 刷新回调队列,支持 before/after flush 钩子
  • 为任务引入优先级队列,处理用户交互优先级与渲染任务竞争
  • 错误边界与任务中断,避免单个任务异常阻塞整次刷新

总结

  • nextTick 本质是对一次“队列刷新微任务”的等待封装
  • 通过 Promise 微任务调度、队列去重与排序,保证一致的更新语义
  • 简化版实现足以帮助理解调度器与 nextTick 的协作关系,可作为学习与调试模板

真实源码视角(结构化)

  • 刷新阶段划分:pre 副作用 → 渲染 jobspost 回调
  • 三类入队:queuePreFlushCbqueueJobqueuePostFlushCb
  • 刷新顺序:先 pre,再主队列 jobs,最后 post
  • 刷新触发:通过一次已解析 Promise 安排微任务,仅当未在刷新且未等待刷新时才安排
简化的三队列调度器
代码语言:javascript
复制
type Fn = () => void

const preCbs: Fn[] = []
const postCbs: Fn[] = []
const jobs: Fn[] = []

let isFlushing = false
let isPending = false
let currentFlushPromise: Promise<void> | null = null
const resolved = Promise.resolve()

export function queuePreFlushCb(cb: Fn) { if (!preCbs.includes(cb)) preCbs.push(cb); schedule() }
export function queueJob(job: Fn) { if (!jobs.includes(job)) jobs.push(job); schedule() }
export function queuePostFlushCb(cb: Fn) { if (!postCbs.includes(cb)) postCbs.push(cb); schedule() }

function schedule() {
  if (!isFlushing && !isPending) { isPending = true; currentFlushPromise = resolved.then(flushAll) }
}

function flushAll() {
  isFlushing = true
  isPending = false
  for (let i = 0; i < preCbs.length; i++) preCbs[i]()
  preCbs.length = 0
  for (let i = 0; i < jobs.length; i++) jobs[i]()
  jobs.length = 0
  for (let i = 0; i < postCbs.length; i++) postCbs[i]()
  postCbs.length = 0
  isFlushing = false
  currentFlushPromise = null
}

export function nextTick<T = void>(fn?: () => T | Promise<T>) {
  const p = currentFlushPromise ?? resolved
  return fn ? p.then(fn) : p
}

微任务实现与降级方案

  • 现代环境优先用 queueMicrotaskPromise.resolve().then
  • 降级可选:MutationObserverMessageChannelsetTimeout
  • 原则:尽量使用微任务,保证刷新在本轮事件循环尽早完成
微任务调度器工厂
代码语言:javascript
复制
type Scheduler = (fn: () => void) => void

export function createMicrotaskScheduler(): Scheduler {
  if (typeof queueMicrotask === 'function') return queueMicrotask
  if (typeof Promise !== 'undefined') {
    const p = Promise.resolve()
    return fn => p.then(fn)
  }
  return fn => setTimeout(fn, 0)
}

实战场景与用法模式

  • 读取 DOM 尺寸:状态更新后 await nextTick() 再读布局
  • 聚焦输入框:切换 v-if 后在 nextTick 中执行 el.focus()
  • 列表动画:批量状态变更合并到一次刷新,避免中间态闪烁
示例:测量与滚动定位
代码语言:javascript
复制
import { ref, nextTick } from 'vue'

const visible = ref(false)
const top = ref(0)

async function openAndMeasure(container: HTMLElement) {
  visible.value = true
  await nextTick()
  top.value = container.getBoundingClientRect().top
}
示例:多次变更合并
代码语言:javascript
复制
import { ref, nextTick } from 'vue'

const count = ref(0)

async function inc3() {
  count.value++
  count.value++
  count.value++
  await nextTick()
}

易错点与最佳实践

  • 在同一事件回调中直接读取 DOM 会读到旧值,应 await nextTick()
  • 不要用 setTimeout 代替 nextTick,宏任务会延后到下一轮,可能影响交互
  • 多个 nextTick 链式调用无需担心顺序,它们共享同一刷新 Promise
  • 组件销毁后再访问 DOM 需判断存在性,避免空引用

进阶:任务优先级与中断

  • 可为任务增加优先级,先执行高优交互相关任务,再渲染低优任务
  • 对执行异常进行捕获与隔离,避免单个任务阻塞整次刷新
简化优先级队列
代码语言:javascript
复制
type Job = { run: () => void; prio: number }

const q: Job[] = []

export function enqueue(job: Job) { q.push(job); scheduleFlush() }

function scheduleFlush() { Promise.resolve().then(flush) }

function flush() {
  q.sort((a, b) => b.prio - a.prio)
  for (let i = 0; i < q.length; i++) q[i].run()
  q.length = 0
}

与 Vue2 的差异简述

  • Vue2 同样基于微任务,但在部分环境会回退到宏任务
  • Vue3 的调度器更模块化,提供 pre/post 队列与明确的刷新阶段
  • 通过 effect 与调度器协作,支持更细粒度的更新合并

常见问题解答

  • 什么时候需要 nextTick:当你依赖 DOM 的最新布局或需要在视图更新后继续逻辑
  • 能否在 watch 回调里省略:若回调仅处理数据无需 DOM,则不必使用
  • 多组件同时更新会乱序吗:通过队列排序与阶段划分保证顺序稳定

小结与练习

  • 练习一:实现三队列调度器并替换示例中的简化实现
  • 练习二:在组件交互中找出必须 nextTick 的场景,验证刷新时机
  • 练习三:为任务加入优先级,观察交互与渲染的先后差异

时序图与事件循环示意

代码语言:javascript
复制
同步代码开始
│
├─ 修改响应式状态
│   └─ 副作用入队 jobs
│
├─ 调用 nextTick
│   └─ 获取当前刷新 Promise p
│
└─ 事件回调结束
    └─ 执行微任务队列
        └─ flushAll/flushJobs
            ├─ preCbs
            ├─ jobs
            └─ postCbs
                └─ DOM 已更新
                    └─ nextTick 回调执行

源码术语与概念映射

  • reactive effect:由响应式驱动的副作用函数
  • scheduler:副作用的调度器,决定何时执行 effect
  • job:一次组件更新或渲染任务
  • pre/post cbs:刷新前后回调队列
  • flush:一次集中处理队列的过程

API 语义与用法对比

  • nextTick(cb):将回调安排在当前刷新后的微任务中执行
  • await nextTick():等待刷新完成后继续执行后续逻辑
  • 二者本质一致,场景上推荐异步函数使用 await,普通回调使用 nextTick(cb)
次序验证片段
代码语言:javascript
复制
Promise.resolve().then(() => console.log('microtask'))
setTimeout(() => console.log('macrotask'))
console.log('sync')

误区澄清

  • nextTick 不等待网络、图片等资源加载,只与渲染刷新相关
  • 在同一事件回调中读取 DOM 不一定需要 nextTick,仅当依赖更新后的布局时才需要
  • 在持续动画或频繁状态变更场景中,不要在每次变更后都调用 nextTick

性能与工程建议

  • 优先批量变更数据,将测量或依赖 DOM 的逻辑收敛到一次 nextTick
  • 合理使用队列拆分,将昂贵的逻辑放入 post 阶段降低阻塞风险
  • 避免在 flush 中执行过长任务,保持每次刷新可控

场景 Cookbook

  • 弹窗打开后测量内容高度并滚动定位
  • 列表渲染后在 post 阶段触发进入动画,避免中间态闪烁
  • 表单切换后在 nextTick 聚焦第一个输入框并选择文本
片段:列表渲染后触发动画
代码语言:javascript
复制
import { ref, nextTick } from 'vue'

const items = ref<number[]>([])

async function addBatch(n: number) {
  const base = items.value.length
  for (let i = 0; i < n; i++) items.value.push(base + i)
  await nextTick()
  document.body.dispatchEvent(new Event('list-enter'))
}

深入:嵌套 nextTick 与刷新合并

  • 同一轮事件循环内多次调用 nextTick 共享同一刷新 Promise
  • 在刷新进行中调用 nextTick,会使用当前正在进行的 Promise,保证时序一致
  • 刷新完成后 currentFlushPromise 重置,下次将指向新的刷新

对比其他异步策略

  • requestAnimationFrame 着重在下一帧执行,适合动画与布局测量的帧级协作
  • setImmediateMessageChannel 可用于宏任务近似场景,但平台兼容性差异较大
  • 综合选择应以微任务刷新优先,帧任务作为补充
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 源码阅读入门:解析 Vue3 的 nextTick 原理(附简化版实现)
    • 背景与现象
    • 原理概览
    • 核心机制(概念)
    • 简化版实现(可运行)
      • 调度器与 nextTick
      • 使用与验证
    • Vue3 中的关系链路(简析)
    • 关键特性与边界
    • 对比常见替代方案
    • 进一步扩展(贴近真实)
    • 总结
    • 真实源码视角(结构化)
      • 简化的三队列调度器
    • 微任务实现与降级方案
      • 微任务调度器工厂
    • 实战场景与用法模式
      • 示例:测量与滚动定位
      • 示例:多次变更合并
    • 易错点与最佳实践
    • 进阶:任务优先级与中断
      • 简化优先级队列
    • 与 Vue2 的差异简述
    • 常见问题解答
    • 小结与练习
    • 时序图与事件循环示意
    • 源码术语与概念映射
    • API 语义与用法对比
      • 次序验证片段
    • 误区澄清
    • 性能与工程建议
    • 场景 Cookbook
      • 片段:列表渲染后触发动画
    • 深入:嵌套 nextTick 与刷新合并
    • 对比其他异步策略
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档