跳至主要內容
Skip to content

nextTick 的作用與實現原理

nextTick 是 Vue 面試裡很容易被追問的題目。很多人知道「等 DOM 更新後再執行」,但如果面試官接著問「為什麼要等?Vue 不是資料一改就更新畫面嗎?」就需要理解 Vue 的更新排程。

一句話回答:nextTick 用來在 Vue 完成當前批次 DOM 更新後執行回呼;它的存在是因為 Vue 會把多次狀態變更合併到同一輪更新中,而不是每次資料一改就立刻同步 patch DOM。


一、 必考觀念

1. 為什麼需要 nextTick

看這段程式:

vue
<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 會印出:

text
hello vue

但它可能還是舊的 DOM 內容:

text
hello

原因是:message.value 是同步改變的,但 DOM 更新不是立刻同步完成。Vue 會把這次更新排進佇列,等目前同步程式跑完後,再批次 patch DOM。

這時就可以用 nextTick

typescript
import { nextTick } from 'vue'

async function updateMessage() {
  message.value = 'hello vue'

  await nextTick()

  console.log(boxRef.value?.textContent) // hello vue
}

2. Vue 為什麼不每次資料改變就立刻更新 DOM?

因為同一段同步程式裡可能會改很多次狀態。

typescript
count.value++
count.value++
count.value++

如果每次都立刻更新 DOM,就會 patch 三次。但實際上畫面只需要顯示最後結果。

Vue 會把更新合併:

text
同步修改多次 state
-> 收集受影響的 component update job
-> 去重
-> 等同步程式結束
-> 批次更新 DOM

這樣可以減少不必要的 DOM patch,也能讓更新順序更可控。

3. nextTick 的作用是什麼?

nextTick 的作用是:等 Vue 把目前這一批更新 flush 完,再執行你的回呼或恢復 await 後面的程式。

常見用法:

typescript
import { nextTick } from 'vue'

await nextTick()

或:

typescript
nextTick(() => {
  // DOM 已完成本輪更新
})

它適合用在「狀態已經改了,但你需要讀更新後 DOM」的場景。

4. nextTick 不是等下一秒

nextTick 不是 setTimeout,也不是等畫面動畫結束。它等的是 Vue 的本輪 DOM 更新。

typescript
visible.value = true
await nextTick()
// DOM 通常已經被 Vue patch
// 但 CSS transition 不一定結束

如果你要等 transition 結束,應該用 transition hooks 或監聽 transitionend,而不是只靠 nextTick


二、 實現原理

1. 核心是更新佇列

Vue 的響應式資料改變後,會觸發依賴它的 effect。對元件來說,這通常會形成一個 component update job。

簡化版:

typescript
function triggerUpdate(job) {
  queueJob(job)
}

queueJob 會把 job 放入佇列,並安排一次 flush:

typescript
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 微任務。

簡化版:

typescript
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. 微任務和宏任務差在哪?

簡化理解:

text
同步程式
-> 微任務 Promise.then / queueMicrotask
-> 瀏覽器渲染
-> 下一個宏任務 setTimeout / click / network

微任務通常會在目前同步程式結束後、瀏覽器進入下一輪渲染前執行。這讓 Vue 可以很快地 flush 更新,而不必等到 setTimeout 那樣的下一個宏任務。

javascript
console.log('sync')

Promise.resolve().then(() => {
  console.log('microtask')
})

setTimeout(() => {
  console.log('macrotask')
})

通常輸出:

text
sync
microtask
macrotask

4. Vue 2 和 Vue 3 的差異

Vue 2 的 nextTick 會根據環境選擇不同 fallback,例如 Promise、MutationObserver、setImmediate、setTimeout 等。

Vue 3 的現代環境基礎更好,主要使用 Promise microtask 來排程。

面試回答不需要背所有 fallback,重點是:

  • nextTick 不是單純 setTimeout
  • 它會盡量用微任務。
  • 目標是等 Vue 本輪批次更新完成後再執行。

三、 追問題庫

Q1:修改資料後,資料本身是同步變嗎?

是。

typescript
count.value = 2
console.log(count.value) // 2

同步變的是響應式狀態本身。非同步的是 DOM patch。

Q2:nextTick 後一定能拿到最新 DOM 嗎?

通常可以拿到 Vue 本輪 patch 後的 DOM。

但要注意:

  • 如果資料更新又觸發新的非同步更新,可能還要再等後續時機。
  • 如果要等 CSS transition 或圖片載入,nextTick 不保證完成。
  • 如果第三方套件自己也有排程,可能要等套件自己的 ready / callback。

Q3:nextTicksetTimeout 有什麼差?

setTimeout 是宏任務,通常比微任務晚。nextTick 是接在 Vue 更新 flush 後的排程,更精準地表達「我要等 Vue DOM 更新完」。

不要用 setTimeout(() => {}, 0) 代替 nextTick。它可能能跑,但語意不精準,也可能帶來不必要延遲。

Q4:什麼時候需要用 nextTick

常見場景:

  • v-if 顯示元素後,立刻 focus。
  • 列表更新後,讀取 DOM 高度。
  • 切換內容後,滾動到指定位置。
  • 更新文字後,測量元素尺寸。
  • 初始化依賴 DOM 的第三方套件。

例如:

typescript
visible.value = true
await nextTick()
inputRef.value?.focus()

因為 visible 控制的 input 可能要等 DOM patch 後才真的存在。

Q5:可以濫用 nextTick 嗎?

不建議。

如果你的邏輯大部分都要靠 nextTick 才能工作,可能代表狀態設計或元件邊界有問題。

優先思考:

  • 能不能用 computed 推導?
  • 能不能用 watch 的 flush: 'post'
  • 能不能把 DOM 操作封裝在子元件內?
  • 是不是第三方套件整合才真的需要 DOM 時機?

Q6:nextTickwatch(..., { flush: 'post' }) 有什麼不同?

nextTick 是你在某段程式裡主動等 DOM 更新。

flush: 'post' 則是讓 watch callback 在元件 DOM 更新後執行。

typescript
watch(
  () => props.value,
  () => {
    // DOM 更新後執行
  },
  { flush: 'post' },
)

如果你是在反應某個狀態變化並讀 DOM,flush: 'post' 可能比到處手動 nextTick 更清楚。


四、 實作題

題目:打開輸入框後自動 focus

需求:

  • 點擊按鈕顯示輸入框。
  • 輸入框出現後自動 focus。

錯誤寫法:

vue
<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

正確寫法:

vue
<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 一開始就存在,問題會比較不一樣:

vue
<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 修補資料流問題

如果你需要寫很多:

typescript
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 元素或測量尺寸。

延伸閱讀