nextTick 的作用與實現原理
nextTick 是 Vue 面試裡很容易被追問的題目。很多人知道「等 DOM 更新後再執行」,但如果面試官接著問「為什麼要等?Vue 不是資料一改就更新畫面嗎?」就需要理解 Vue 的更新排程。
一句話回答:nextTick 用來在 Vue 完成當前批次 DOM 更新後執行回呼;它的存在是因為 Vue 會把多次狀態變更合併到同一輪更新中,而不是每次資料一改就立刻同步 patch DOM。
一、 必考觀念
1. 為什麼需要 nextTick?
看這段程式:
<template>
<div ref="boxRef">{{ message }}</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const message = ref('hello')
const boxRef = ref<HTMLElement | null>(null)
function updateMessage() {
message.value = 'hello vue'
console.log(boxRef.value?.textContent)
}
</script>直覺上,你可能以為 console.log 會印出:
hello vue但它可能還是舊的 DOM 內容:
hello原因是:message.value 是同步改變的,但 DOM 更新不是立刻同步完成。Vue 會把這次更新排進佇列,等目前同步程式跑完後,再批次 patch DOM。
這時就可以用 nextTick:
import { nextTick } from 'vue'
async function updateMessage() {
message.value = 'hello vue'
await nextTick()
console.log(boxRef.value?.textContent) // hello vue
}2. Vue 為什麼不每次資料改變就立刻更新 DOM?
因為同一段同步程式裡可能會改很多次狀態。
count.value++
count.value++
count.value++如果每次都立刻更新 DOM,就會 patch 三次。但實際上畫面只需要顯示最後結果。
Vue 會把更新合併:
同步修改多次 state
-> 收集受影響的 component update job
-> 去重
-> 等同步程式結束
-> 批次更新 DOM這樣可以減少不必要的 DOM patch,也能讓更新順序更可控。
3. nextTick 的作用是什麼?
nextTick 的作用是:等 Vue 把目前這一批更新 flush 完,再執行你的回呼或恢復 await 後面的程式。
常見用法:
import { nextTick } from 'vue'
await nextTick()或:
nextTick(() => {
// DOM 已完成本輪更新
})它適合用在「狀態已經改了,但你需要讀更新後 DOM」的場景。
4. nextTick 不是等下一秒
nextTick 不是 setTimeout,也不是等畫面動畫結束。它等的是 Vue 的本輪 DOM 更新。
visible.value = true
await nextTick()
// DOM 通常已經被 Vue patch
// 但 CSS transition 不一定結束如果你要等 transition 結束,應該用 transition hooks 或監聽 transitionend,而不是只靠 nextTick。
二、 實現原理
1. 核心是更新佇列
Vue 的響應式資料改變後,會觸發依賴它的 effect。對元件來說,這通常會形成一個 component update job。
簡化版:
function triggerUpdate(job) {
queueJob(job)
}queueJob 會把 job 放入佇列,並安排一次 flush:
const queue = []
let isFlushPending = false
function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job)
}
if (!isFlushPending) {
isFlushPending = true
Promise.resolve().then(flushJobs)
}
}這裡的 Promise.resolve().then(flushJobs) 就是把 flush 放到微任務中。
2. 為什麼常說 nextTick 基於微任務?
Vue 3 中,nextTick 主要基於 Promise 微任務。
簡化版:
const resolvedPromise = Promise.resolve()
let currentFlushPromise = null
function nextTick(fn) {
const promise = currentFlushPromise || resolvedPromise
return fn ? promise.then(fn) : promise
}如果目前有正在等待 flush 的更新,nextTick 會接在那個 flush promise 後面;如果沒有 pending flush,就接在一個已 resolve 的 Promise 後面。
所以它能做到:讓你的回呼排在 Vue 本輪 DOM 更新之後。
IMPORTANT
這裡是面試理解版,不是逐字複製 Vue 原始碼。真正實作還包含 job 排序、遞迴更新檢查、pre flush / post flush callback 等細節。
3. 微任務和宏任務差在哪?
簡化理解:
同步程式
-> 微任務 Promise.then / queueMicrotask
-> 瀏覽器渲染
-> 下一個宏任務 setTimeout / click / network微任務通常會在目前同步程式結束後、瀏覽器進入下一輪渲染前執行。這讓 Vue 可以很快地 flush 更新,而不必等到 setTimeout 那樣的下一個宏任務。
console.log('sync')
Promise.resolve().then(() => {
console.log('microtask')
})
setTimeout(() => {
console.log('macrotask')
})通常輸出:
sync
microtask
macrotask4. Vue 2 和 Vue 3 的差異
Vue 2 的 nextTick 會根據環境選擇不同 fallback,例如 Promise、MutationObserver、setImmediate、setTimeout 等。
Vue 3 的現代環境基礎更好,主要使用 Promise microtask 來排程。
面試回答不需要背所有 fallback,重點是:
nextTick不是單純setTimeout。- 它會盡量用微任務。
- 目標是等 Vue 本輪批次更新完成後再執行。
三、 追問題庫
Q1:修改資料後,資料本身是同步變嗎?
是。
count.value = 2
console.log(count.value) // 2同步變的是響應式狀態本身。非同步的是 DOM patch。
Q2:nextTick 後一定能拿到最新 DOM 嗎?
通常可以拿到 Vue 本輪 patch 後的 DOM。
但要注意:
- 如果資料更新又觸發新的非同步更新,可能還要再等後續時機。
- 如果要等 CSS transition 或圖片載入,
nextTick不保證完成。 - 如果第三方套件自己也有排程,可能要等套件自己的 ready / callback。
Q3:nextTick 和 setTimeout 有什麼差?
setTimeout 是宏任務,通常比微任務晚。nextTick 是接在 Vue 更新 flush 後的排程,更精準地表達「我要等 Vue DOM 更新完」。
不要用 setTimeout(() => {}, 0) 代替 nextTick。它可能能跑,但語意不精準,也可能帶來不必要延遲。
Q4:什麼時候需要用 nextTick?
常見場景:
v-if顯示元素後,立刻 focus。- 列表更新後,讀取 DOM 高度。
- 切換內容後,滾動到指定位置。
- 更新文字後,測量元素尺寸。
- 初始化依賴 DOM 的第三方套件。
例如:
visible.value = true
await nextTick()
inputRef.value?.focus()因為 visible 控制的 input 可能要等 DOM patch 後才真的存在。
Q5:可以濫用 nextTick 嗎?
不建議。
如果你的邏輯大部分都要靠 nextTick 才能工作,可能代表狀態設計或元件邊界有問題。
優先思考:
- 能不能用 computed 推導?
- 能不能用 watch 的
flush: 'post'? - 能不能把 DOM 操作封裝在子元件內?
- 是不是第三方套件整合才真的需要 DOM 時機?
Q6:nextTick 和 watch(..., { flush: 'post' }) 有什麼不同?
nextTick 是你在某段程式裡主動等 DOM 更新。
flush: 'post' 則是讓 watch callback 在元件 DOM 更新後執行。
watch(
() => props.value,
() => {
// DOM 更新後執行
},
{ flush: 'post' },
)如果你是在反應某個狀態變化並讀 DOM,flush: 'post' 可能比到處手動 nextTick 更清楚。
四、 實作題
題目:打開輸入框後自動 focus
需求:
- 點擊按鈕顯示輸入框。
- 輸入框出現後自動 focus。
錯誤寫法:
<template>
<button @click="open">新增</button>
<input v-if="visible" ref="inputRef" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
const visible = ref(false)
const inputRef = ref<HTMLInputElement | null>(null)
function open() {
visible.value = true
inputRef.value?.focus()
}
</script>問題是 visible.value = true 後,input 還沒被 patch 到 DOM,inputRef.value 可能還是 null。
正確寫法:
<template>
<button @click="open">新增</button>
<input v-if="visible" ref="inputRef" />
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
const visible = ref(false)
const inputRef = ref<HTMLInputElement | null>(null)
async function open() {
visible.value = true
await nextTick()
inputRef.value?.focus()
}
</script>如果用 v-show,DOM 一開始就存在,問題會比較不一樣:
<input v-show="visible" ref="inputRef" />但如果顯示切換後要讀 layout,仍可能需要等 Vue 完成本輪 patch。
五、 資深視角
1. nextTick 是更新排程的出口
資淺會把 nextTick 當成「等 DOM」。
資深一點要知道,它背後其實是 Vue 的更新排程:
- 狀態改變觸發 effect。
- 元件 update job 被排入 queue。
- queue 會去重與排序。
- flush 時 patch DOM。
nextTick接在 flush promise 後面。
所以 nextTick 不是獨立魔法,而是整個 renderer scheduler 的一部分。
2. 不要用 nextTick 修補資料流問題
如果你需要寫很多:
await nextTick()
await nextTick()通常要停下來檢查:
- 是不是狀態來源不清楚?
- 是不是父子元件更新順序被你硬綁在一起?
- 是不是 DOM 操作應該封裝在子元件?
- 是不是應該用 watch post flush?
nextTick 應該用來處理「真的需要 DOM 更新後時機」的事情,而不是拿來讓混亂的狀態流程剛好能跑。
3. 理解它能幫你看懂效能問題
Vue 批次更新是為了避免每次狀態變更都立刻 patch DOM。這跟「DOM 操作成本」和「Diff 算法」是同一條線上的知識。
如果你知道:
- DOM patch 有成本。
- Vue 會批次更新。
- Diff 會找出必要變更。
nextTick是等待這批更新完成。
你就能比較完整地解釋 Vue 為什麼不是「資料一改,DOM 立刻同步改」。
總結
| 問題 | 面試回答重點 |
|---|---|
nextTick 作用 | 等 Vue 本輪 DOM 更新完成後執行 |
| 為什麼需要 | Vue 會批次合併狀態更新,不會每次都同步 patch DOM |
| 狀態是否同步 | 響應式狀態本身同步改變 |
| DOM 是否同步 | DOM 更新是排程後批次執行 |
| 實現基礎 | 更新佇列 + Promise 微任務 |
| 常見場景 | v-if 後 focus、列表更新後量尺寸、滾動定位 |
一句完整的面試回答可以是:
nextTick的作用是在 Vue 完成本輪 DOM 更新後執行回呼。Vue 中響應式狀態的修改是同步的,但 DOM patch 會被排入更新佇列,並在目前同步程式結束後批次 flush,這樣可以去重並減少不必要的 DOM 更新。nextTick通常基於 Promise 微任務,會接在 Vue 的 flush promise 後面,所以適合在資料變更後讀取最新 DOM、focus 元素或測量尺寸。