nextTick

很多人第一次接触 nextTick 时,都会把它理解成一句话:

DOM 更新后执行一段代码。

这个理解不算错,但太简化了。

更准确地说,nextTick 解决的是这样一个问题:

你已经修改了响应式数据,但 Vue 并不会立刻同步改 DOM,而是会把更新合并起来,等当前这轮任务结束后再统一刷新。nextTick 就是让你在“这次 DOM 刷新完成之后”拿到一个可靠时机。

上图先记住一个核心结论:nextTick 一定排在“DOM 刷新之后”,但它依赖的是 Vue 的更新队列,不是普通意义上的“延迟一下”。

一、为什么需要 nextTick

先看一个最常见的例子:

const visible = ref(false)
const boxRef = ref<HTMLElement | null>(null)

function open() {
  visible.value = true
  console.log(boxRef.value)
}

很多人会以为点完按钮后 boxRef.value 已经能拿到了,但实际上这里经常还是 null

原因不是 Vue 慢,而是 Vue 在做一件很合理的事:

  • 你刚改了状态
  • Vue 先记录“这里需要更新”
  • 等当前同步代码跑完,再统一执行渲染

所以这段代码真正的执行顺序更像:

  1. visible.value = true
  2. Vue 记录组件需要重新渲染
  3. console.log(boxRef.value) 立刻执行
  4. 当前同步任务结束
  5. Vue 批量刷新 DOM

因此这时候正确写法通常是:

import { nextTick, ref } from 'vue'

const visible = ref(false)
const boxRef = ref<HTMLElement | null>(null)

async function open() {
  visible.value = true
  await nextTick()
  console.log(boxRef.value)
}

二、不要把 nextTick 理解成“延迟执行”

这是初学者最常见的误区之一。

nextTick 不是“随便晚一点执行”,也不是简单的 setTimeout(fn, 0)

它真正表达的是:

  • 等待 Vue 完成本轮更新队列
  • 在 DOM 已经和最新状态同步后再执行回调

也就是说,它关心的是 Vue 的更新时机,不是普通的“时间延后”。

三、Vue 为什么不立刻更新 DOM

因为立刻更新会非常低效。

假设你在一个函数里连续改 3 次状态:

state.count++
state.title = 'hello'
state.visible = true

如果每改一次都立刻去操作 DOM,会发生很多重复工作。

更合理的方式是:

  • 先把这些修改记下来
  • 把相关组件放进更新队列
  • 当前同步代码结束后统一刷新一次

这就是 Vue 的“异步批量更新”思想。

四、一个更贴近直觉的理解方式

你可以把 Vue 想象成一个认真工作的前台:

  • 你连续说了 3 件事
  • 它不会你说一句就立刻跑一次后台
  • 它会先记下来
  • 等你说完,再一次性处理

nextTick 就像你说:

等你把这批事情处理完,再叫我。

五、什么时候该用 nextTick

1. 修改状态后,立刻读取最新 DOM

这是最典型的场景。

比如:

  • 打开弹窗后获取弹窗节点
  • 列表更新后读取高度
  • 输入框出现后自动聚焦
  • 切换 tab 后滚动到某个位置

2. 依赖最新渲染结果做后续计算

例如:

  • 根据最新内容计算容器高度
  • 根据最新节点位置执行动画
  • 等表格渲染后初始化第三方库

3. 某些测试场景中等待更新完成

在组件测试里也经常会用到:

await nextTick()

因为断言之前往往要先等视图更新完成。

六、什么时候不该滥用 nextTick

1. 只是为了“代码能跑”而机械加上

如果你发现自己到处都在写:

await nextTick()

那往往说明你没有搞清楚真正的问题。

可能的根因其实是:

  • 组件设计不合理
  • 生命周期用错了
  • 状态和 DOM 耦合太重
  • 本该用计算属性,却写成了手动操作 DOM

2. 用它代替正常的数据流设计

nextTick 是时机控制工具,不是架构补丁。

如果一个逻辑只有在不停加 nextTick 后才勉强成立,通常说明这段逻辑需要重新设计。

七、源码层面,nextTick 到底在做什么

从源码思路上看,nextTick 本身并不复杂。核心就是两件事:

  1. 把回调放进一个队列
  2. 选择一个合适的异步时机统一清空队列

一个极简版实现可以写成这样:

const callbacks: Array<() => void> = []
let pending = false

function flushCallbacks() {
  pending = false
  const copies = callbacks.slice()
  callbacks.length = 0
  for (const cb of copies) {
    cb()
  }
}

export function nextTick(cb?: () => void) {
  if (cb) callbacks.push(cb)

  if (!pending) {
    pending = true
    Promise.resolve().then(flushCallbacks)
  }
}

这段代码已经能体现 nextTick 的核心思想:

  • 多次调用不会重复创建很多异步任务
  • 所有回调都会被合并到同一轮里执行

八、为什么优先使用微任务

在现代实现里,nextTick 通常优先使用微任务,例如:

Promise.resolve().then(flushCallbacks)

因为微任务执行时机更早,也更适合和组件更新队列配合。

你不需要死记事件循环的所有细节,但至少要知道:

  • nextTick 不是随便找个异步 API
  • 它会尽量选择更适合“本轮更新结束后立刻执行”的机制

九、Vue 2 和 Vue 3 的思路有什么区别

Vue 2

nextTick 更像一个独立的“回调队列调度工具”。

它会:

  • 收集回调
  • 选择异步方式
  • 统一执行回调

Vue 3

Vue 3 仍然保留了 nextTick,但它和整个调度系统联系得更紧了。

因为 Vue 3 的响应式、副作用调度、组件更新机制整体更统一,所以 nextTick 常常是和“刷新任务队列”一起理解的。

你可以把 Vue 3 理解成:

  • 组件更新由调度器统一安排
  • nextTick 让你等这轮调度完成

十、和更新队列的关系

如果把源码主线再压缩一点,可以理解成:

  1. 你修改响应式数据
  2. 触发组件更新任务入队
  3. 调度器安排刷新
  4. DOM 更新完成
  5. nextTick 回调执行

这也是为什么我们经常说:

nextTick 等的不是“时间”,等的是“这一轮更新结束”。

十一、一个简单的源码脉络

可以用下面这段伪代码帮助理解:

const queue: Job[] = []
let isFlushing = false
const resolvedPromise = Promise.resolve()

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

function queueFlush() {
  if (isFlushing) return
  isFlushing = true
  resolvedPromise.then(flushJobs)
}

function flushJobs() {
  try {
    for (const job of queue) {
      job()
    }
  } finally {
    queue.length = 0
    isFlushing = false
  }
}

export function nextTick(fn?: () => void) {
  return fn ? resolvedPromise.then(fn) : resolvedPromise
}

真实源码当然会处理更多边界,例如:

  • 任务去重
  • 递归更新保护
  • 前置/后置队列
  • 错误处理

但主线思想就是:调度刷新 -> 刷新结束 -> nextTick 接上

十二、一个最常见的实际场景

比如你要在弹窗出现后自动聚焦输入框:

<script setup lang="ts">
import { nextTick, ref } from 'vue'

const visible = ref(false)
const inputRef = ref<HTMLInputElement | null>(null)

async function openDialog() {
  visible.value = true
  await nextTick()
  inputRef.value?.focus()
}
</script>

这里 nextTick 的意义不是“延迟 focus”,而是:

  • 先让输入框真正渲染出来
  • 再去调用 DOM API

十三、如何判断要不要用 nextTick

你可以问自己两个问题:

1. 我是不是在改完状态后立刻依赖 DOM?

如果是,很可能需要。

2. 我依赖的是最新渲染结果,而不是最新数据吗?

如果是,也很可能需要。

如果你只是依赖数据本身,那通常不需要 nextTick

总结

nextTick 的本质不是一个“延迟函数”,而是 Vue 更新机制的一部分。

记住下面这句话就够了:

  • 改状态,不等于 DOM 立刻更新
  • nextTick 等的是这轮 DOM 更新完成

理解了这点,很多关于 nextTick 的“玄学问题”都会变得很清楚。

上次更新:
贡献者: Joe, joe