在写 Vue 项目的过程中,你可能遇到过这样的情况:明明刚刚改了数据,但紧接着读取 DOM 时,拿到的还是旧的状态。这时候就需要用到 $nextTick。
1、先理解 Vue 更新 DOM 的节奏
Vue 有一个核心特性:数据变了,DOM 不会立刻更新。
打个比方:Vue 就像一个餐厅的厨师,你(JS 代码)是点单的服务员。
- 你说「把
isLoading改成false」→ 厨师把这个任务记到小本子上 - 你说「再把
title改成 '加载完成'」→ 厨师继续记 - 等你这一轮点单说完了(当前这段 JS 执行完毕)→ 厨师才一次性把所有变化都做到 DOM 上
这个"一次性处理"的动作,就叫做一次 tick(更新批次) 。
Vue 这么设计是为了性能——如果你改了 10 个数据,不用刷新 DOM 10 次,合并成 1 次就够了(关于这个设计的详细原因,可以看《Vue 为什么选择异步更新 DOM》这篇文章)。
2、$nextTick 是什么
$nextTick(callback) 的意思就是:
"等厨师把这一批菜都做完、端上桌之后,再执行我的回调。"
注意关键词是这一批:
- ✅ 等:Vue 把当前已排队的 DOM 更新全部刷新完
- ❌ 不等:接口请求返回、图片加载完成、CSS 动画结束
3、用代码来感受一下
不用 $nextTick 的问题:
export default {
data() {
return {
showBox: false
}
},
methods: {
handleClick() {
this.showBox = true
// ❌ 此时 DOM 还没更新,box 元素可能还不存在
const box = document.querySelector('.box')
console.log(box) // null 或者旧状态
}
}
}
用 $nextTick 修复:
methods: {
handleClick() {
this.showBox = true
// ✅ 等 Vue 把 DOM 更新完,再读取元素
this.$nextTick(() => {
const box = document.querySelector('.box')
console.log(box) // 正确拿到元素
})
}
}
另一个常见场景——展开折叠后检测元素位置:
methods: {
expandPanel() {
this.isExpanded = true
// 此时 DOM 里折叠区域还没展开
this.$nextTick(() => {
// 现在 DOM 已经更新,可以准确读取元素位置
const el = this.$refs.panel
console.log(el.getBoundingClientRect())
})
}
}
如果不加 $nextTick,直接读 getBoundingClientRect(),拿到的还是折叠状态的尺寸,是错误的数据。
4、执行顺序拆解
① this.isExpanded = true ← 改数据,Vue 记入队列
② this.title = '已展开' ← 改数据,Vue 记入队列
③ $nextTick(callback) 登记 ← 告诉 Vue:"队列刷完叫我"
--- 当前这段 JS 执行完毕 ---
④ Vue 把 ①② 产生的所有 DOM 变化一次性刷新
⑤ 执行 callback(你的代码)
callback 里的逻辑一定在 DOM 更新之后执行,这是 Vue 的保证。
5、什么时候需要用 $nextTick
| 场景 | 是否需要 $nextTick |
|---|---|
| 改完数据后,需要读取 DOM 尺寸/位置 | ✅ 需要 |
| 改完数据后,需要操作 DOM 元素(聚焦、滚动等) | ✅ 需要 |
改完数据后,需要通过 $refs 调用子组件方法 | ✅ 需要 |
| 只是改数据,不需要立刻读 DOM | ❌ 不需要 |
| 等接口返回后更新数据 | ❌ 不需要(接口回调本身就是异步的) |
6、Vue 3 中的写法
Vue 3 中用法基本相同,也可以用 await 方式:
import { nextTick } from 'vue'
async function handleClick() {
isExpanded.value = true
await nextTick()
// 此时 DOM 已经更新
console.log(el.value.getBoundingClientRect())
}
7、一句话总结
$nextTick 是 Vue 提供的一个时机钩子,让你能在"数据改完 → DOM 更新完毕"这个节点插入自己的逻辑。它等的不是所有异步操作,只等 Vue 自己的这一轮 DOM 刷新。