Vue3 nextTick到底怎么用?搞懂这5个核心场景才算真会
你有没有在Vue3开发中遇到过这种情况:刚用ref或reactive改了数据,紧接着去取DOM元素的尺寸、位置或者内容,结果拿到的还是旧值?比如写了个列表渲染,数据push了新项后去数列表DOM的长度,居然比数据数组少一个?这时候大概率别人会告诉你“用nextTick呀”,但到底为什么要用到它?它是怎么解决问题的?除了这种最基础的场景,还有哪些地方非用不可?今天咱们就把这些问题掰扯得明明白白。
Vue3的DOM更新机制到底是什么?搞懂原理才敢随便用nextTick
很多人用nextTick都是“知其然不知其所以然”,每次遇到DOM获取旧值就无脑加,但其实只要摸透Vue3的DOM更新策略,就能准确判断什么时候需要、什么时候完全没必要。
Vue3的核心响应式原理大家应该都略知一二:通过Proxy代理数据,当数据发生变化时,会触发依赖这个数据的组件重新渲染,但这里的“重新渲染”不是同步的——不是你改一行数据,它立刻就去改DOM,Vue3做了一个“批量异步更新队列”的优化:在同一个事件循环(Event Loop)中,不管你改了多少次数据,哪怕是循环赋值了100次,它只会收集一次需要更新的组件,把它们推到异步队列里,等当前同步代码执行完,微任务阶段再统一处理DOM更新。
为什么要这么做?主要是为了性能,DOM操作是前端最昂贵的操作之一,如果每次改数据都立刻重绘重排,浏览器的CPU和渲染性能会直接拉胯,尤其是处理大量数据或者复杂组件的时候,举个简单的例子:你要把一个数组里的100个元素都替换成新的,如果同步更新,就会触发100次组件的虚拟DOM diff和真实DOM更新;但用了异步队列,只会在所有数据修改完之后做1次,差了整整99倍的性能消耗。
nextTick在Vue3中的基本定义和写法是什么?两种用法都得掌握
那nextTick的作用就很明确了:它就是一个钩子函数,能让你在Vue完成下一次DOM更新批量操作之后,再去执行你指定的回调函数,这时候你拿到的DOM状态肯定是最新的。
在Vue3中,nextTick有两种常用的写法,适应不同的场景:
第一种:回调函数写法(兼容Vue2习惯,写法最直接)
这种写法和Vue2基本没区别,直接从vue包里引入nextTick函数,然后把要执行的代码放在回调里,适合只需要在DOM更新后做一两件简单的事,或者不想处理Promise的场景。
比如刚才提到的列表渲染加元素后数长度的问题:
import { ref, nextTick } from 'vue'
export default {
setup() {
const list = ref([1, 2, 3])
const addItem = () => {
list.value.push(4)
// 这里直接取,拿到的还是3
console.log(document.querySelectorAll('.list-item').length)
nextTick(() => {
// 这里拿到的就是4了,因为DOM已经更新
console.log(document.querySelectorAll('.list-item').length)
})
}
return { list, addItem }
}
}
第二种:Promise写法(Vue3官方更推荐,支持async/await,代码更优雅)
Vue3把nextTick封装成了一个返回Promise的函数,所以你可以用async/await来写,代码逻辑更清晰,不会出现回调地狱的情况,尤其是需要多次操作DOM或者依赖多个nextTick结果的时候,这种写法优势特别明显。
同样的例子用Promise/async-await改写:
import { ref, nextTick } from 'vue'
export default {
setup() {
const list = ref([1, 2, 3])
const addItem = async () => {
list.value.push(4)
console.log(document.querySelectorAll('.list-item').length) // 3
await nextTick()
console.log(document.querySelectorAll('.list-item').length) // 4
// 还可以继续加DOM操作,比如给新添加的元素加个动画
document.querySelector('.list-item:last-child').classList.add('animate__fadeIn')
}
return { list, addItem }
}
}
nextTick在Vue3中的5个核心高频应用场景,每个都有实际代码示例
光知道原理和写法还不够,得知道什么时候该用,下面这5个场景是开发中遇到最多的,掌握了这些,你就能游刃有余地用nextTick了。
获取/操作更新后的DOM元素尺寸、位置、内容
这是nextTick最基础、最常用的场景,除了刚才的数列表长度,还有很多类似的需求,
- 给动态生成的图片添加懒加载占位符(需要先知道图片容器的尺寸)
- 拖拽元素结束后自动滚动到元素可见区域(需要知道元素的位置)
- 文本框输入完内容后自动聚焦到底部的新文本(比如聊天软件)
- 轮播图数据更新后自动重置当前索引(需要知道新的轮播项数量)
我们拿“聊天软件输入完消息自动滚动到底部”来做个更贴近实际的例子:
import { ref, nextTick } from 'vue'
export default {
setup() {
const messageList = ref(['你好', '今天天气怎么样', '听说Vue3很好用'])
const newMessage = ref('')
const scrollContainer = ref(null)
const sendMessage = async () => {
if (!newMessage.value.trim()) return
messageList.value.push(newMessage.value.trim())
newMessage.value = ''
// 滚动容器需要用ref绑定,因为setup里没有this.$refs
// 先更新数据,再等DOM更新
await nextTick()
// 直接把滚动高度设为容器的总高度,就能滚动到底部
scrollContainer.value.scrollTop = scrollContainer.value.scrollHeight
}
return { messageList, newMessage, scrollContainer, sendMessage }
}
}
对应的模板:
<template>
<div class="chat-container">
<div class="message-list" ref="scrollContainer">
<div v-for="(msg, idx) in messageList" :key="idx" class="message-item">{{ msg }}</div>
</div>
<div class="input-area">
<input v-model="newMessage" placeholder="输入消息..." @keyup.enter="sendMessage">
<button @click="sendMessage">发送</button>
</div>
</div>
</template>
初始化时获取/操作动态生成的DOM
有些时候,DOM不是通过用户操作动态生成的,而是组件初始化时通过异步请求获取数据后生成的,这时候直接在setup的同步代码里或者onMounted里取DOM,也有可能拿到空值或者旧值。
为什么onMounted也可能不行?因为onMounted只是保证组件的“根DOM”已经挂载到页面上了,但如果组件内部的某些子DOM是通过v-if、v-for或者异步组件生成的,那么onMounted触发的时候,这些子DOM可能还没渲染出来。
比如我们有一个表格组件,数据是通过接口异步获取的,我们需要在表格完全渲染出来后,给第一行第一列的单元格加个高亮:
import { ref, nextTick, onMounted } from 'vue'
import axios from 'axios'
export default {
setup() {
const tableData = ref([])
const getTableData = async () => {
try {
const res = await axios.get('/api/table-data')
tableData.value = res.data.list
} catch (err) {
console.error('获取表格数据失败', err)
}
}
const highlightFirstCell = async () => {
// 先等接口请求完成,数据更新
await getTableData()
// 再等DOM更新
await nextTick()
const firstCell = document.querySelector('.table-container tr:first-child td:first-child')
if (firstCell) firstCell.style.backgroundColor = '#ffeb3b'
}
onMounted(() => {
// 不能直接在onMounted里写highlightFirstCell的同步代码,必须用async/await
highlightFirstCell()
})
return { tableData }
}
}
强制触发Vue的DOM更新(极少用,但关键时刻能救命)
虽然Vue的响应式系统已经很完善了,但有时候还是会遇到一些“响应式失效”的情况,
- 直接修改数组的索引(比如
arr[0] = 'newVal',不过Vue3用Proxy已经解决了这个问题,主要是Vue2遗留的坑) - 直接添加对象的新属性(Vue3也解决了,但如果用的是shallowRef或者shallowReactive,还是会失效)
- 修改了某些第三方UI库内部的非响应式数据
这时候除了用ref重新赋值整个对象/数组,还可以用nextTick强制触发一次DOM更新——不过要注意,这只是“临时救命”的办法,最好还是找到响应式失效的根本原因,用正确的方式处理数据。
比如用shallowReactive创建的对象,直接添加新属性不会触发更新,这时候可以用nextTick配合强制更新(不过更推荐给shallowReactive的根对象重新赋值,或者用reactive):
import { shallowReactive, nextTick, getCurrentInstance } from 'vue'
export default {
setup() {
// 只有根属性是响应式的,根属性内部的变化或者新根属性的添加不会触发更新
const user = shallowReactive({ name: '张三' })
const { proxy } = getCurrentInstance()
const addAge = async () => {
// 直接添加新根属性,不会触发更新
user.age = 18
console.log(proxy.user) // 能看到age:18,但页面上没显示
// 强制触发一次更新(临时办法)
await nextTick()
proxy.$forceUpdate() // Vue3中$forceUpdate还能用,但仅限根组件或者通过getCurrentInstance获取的proxy
}
return { user, addAge }
}
}
配合v-if/v-show切换DOM后立即操作
v-if是“真正的条件渲染”,会销毁和重建DOM;v-show是“切换CSS的display属性”,不会销毁DOM,不管是哪一种,在切换条件后立即操作DOM,都需要用nextTick。
比如我们有一个弹窗组件,点击按钮打开弹窗(v-if="isShow"),打开后需要自动聚焦到弹窗里的输入框:
import { ref, nextTick } from 'vue'
export default {
setup() {
const isShow = ref(false)
const inputRef = ref(null)
const openModal = async () => {
isShow.value = true
// 直接取inputRef.value,可能是null(v-if刚设为true,DOM还没重建)
await nextTick()
inputRef.value.focus()
}
return { isShow, inputRef, openModal }
}
}
对应的模板:
<template>
<button @click="openModal">打开弹窗</button>
<div v-if="isShow" class="modal">
<input ref="inputRef" placeholder="请输入内容...">
</div>
</template>
如果用v-show的话,虽然DOM不会销毁,但第一次打开的时候(v-show从true变成false再变成true?不,第一次是false→true,DOM也是刚从隐藏变显示吗?不对,v-show第一次加载的时候,如果是false,display就是none,但DOM是已经挂载了的,不过为了保险起见,不管是v-if还是v-show,切换条件后操作DOM都建议加nextTick,尤其是第三方UI库的组件。
结合watch/watchEffect监听数据变化后操作DOM
有时候我们需要监听某个数据的变化,变化后立即操作DOM,这时候也需要用nextTick,因为watch/watchEffect的回调是在数据变化后、DOM更新前触发的(除非你设置了flush: 'post')。
哦对了,这里要提一下Vue3 watch/watchEffect的flush选项,它有三个值:
- 'pre'(默认值):在数据变化后、DOM更新前触发
- 'post':在DOM更新后触发
- 'sync':同步触发,慎用,性能差
如果你不想用nextTick,也可以直接把flush设为'post',效果是一样的,不过用nextTick更灵活,尤其是在watch回调里需要做一些同步操作之后再操作DOM的情况。
比如我们要监听输入框的内容变化,内容长度超过10个字符时,把输入框的边框改成红色:
import { ref, watch, nextTick } from 'vue'
export default {
setup() {
const inputValue = ref('')
const inputRef = ref(null)
// 方式一:用默认的flush: 'pre' + nextTick
watch(inputValue, async (newVal) => {
// 先做一些同步验证
const isInvalid = newVal.length > 10
console.log('输入内容是否过长:', isInvalid)
// 等DOM更新
await nextTick()
if (isInvalid) {
inputRef.value.style.border = '2px solid #f44336'
} else {
inputRef.value.style.border = '1px solid #ccc'
}
})
// 方式二:用flush: 'post',不需要nextTick
// watch(inputValue, (newVal) => {
// const isInvalid = newVal.length > 10
// if (isInvalid) {
// inputRef.value.style.border = '2px solid #f44336'
// } else {
// inputRef.value.style.border = '1px solid #ccc'
// }
// }, { flush: 'post' })
return { inputValue, inputRef }
}
}
使用nextTick时的3个常见误区,别再踩坑了
虽然nextTick很好用,但也不能乱用,下面这3个误区是很多新手甚至是有经验的开发者都会踩的:
所有操作DOM的地方都加nextTick
很多人因为之前踩过获取旧DOM的坑,就养成了“只要操作DOM就加nextTick”的习惯,这其实是完全没必要的,nextTick会把回调推到微任务队列里,虽然微任务执行很快,但还是会有一点点延迟,而且代码里到处都是nextTick,也会影响可读性。
那什么时候不需要加nextTick呢?
- 操作的是静态DOM(不会因为数据变化而改变的DOM)
- 操作的是之前已经通过nextTick或者flush: 'post'确保更新过的DOM
- 在onMounted里操作的是组件的静态根DOM或者不会延迟渲染的子DOM
在nextTick的回调里再次修改数据,不加新的nextTick
如果你在nextTick的回调里再次修改了依赖DOM的数据,那么这个修改触发的DOM更新还是异步的,如果你紧接着又要操作这个新的DOM,还需要再加一个nextTick。
import { ref, nextTick } from 'vue'
export default {
setup() {
const count = ref(0)
const countRef = ref(null)
const doubleCount = async () => {
count.value = 5
await nextTick()
console.log(countRef.value.textContent) // 5
// 再次修改数据
count.value = count.value * 2
// 这里直接取,还是5
console.log(countRef.value.textContent)
// 必须再加一个nextTick
await nextTick()
console.log(countRef.value.textContent) // 10
}
return { count, countRef, doubleCount }
}
}
滥用$nextTick或者全局的nextTick,而不是用flush: 'post'
刚才提到了,Vue3的watch/watchEffect有flush: 'post'选项,它的作用就是让回调在DOM更新后触发,完全可以替代watch/watchEffect里的nextTick,代码更简洁,也更符合Vue的设计理念。
在Vue3的setup里,尽量不要用getCurrentInstance获取的proxy.$nextTick,而是直接从vue包里引入nextTick函数,因为proxy.$nextTick主要是为了兼容Vue2的this.$nextTick,在setup里用原生的nextTick更规范。
Vue3 nextTick是性能优化的产物,用对场景才是关键
最后咱们来总结一下:
- 原理:Vue3的DOM更新是批量异步的,nextTick能让你在DOM更新完成后执行回调
- 写法:有回调函数和Promise/async-await两种,推荐用后者
- 核心场景:获取更新后的DOM、初始化时获取动态DOM、强制更新(临时)、配合v-if/v-show、结合watch/watchEffect
- 常见误区:不要到处加、再次修改数据要加新的、能用flush: 'post'就别用nextTick
掌握了这些,你就能在Vue3开发中灵活运用nextTick了,再也不会遇到“DOM获取旧值”的尴尬情况,最好的办法还是尽量减少直接操作DOM的次数,多用Vue的响应式数据和指令,比如v-bind:style、v-bind:class、v-model等等,这才是Vue的核心优势所在。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


