程式把計數改成1,接著立刻讀畫面的文字,結果還是0。資料已經更新,畫面不是每一行JavaScript都立即重畫。Vue會把同一輪變動的DOM更新整理成批次,等更新完成後,讀到的才是新畫面。
這種「資料是新的,DOM還是舊的」很常出現在展開面板後量尺寸、增加清單後捲到最後、或切換顯示條件後要聚焦輸入欄位。若下一步需要Vue剛產生的DOM,先改資料,再 await nextTick(),最後才讀或操作元素。它等的是Vue的更新批次,不是一個任意的延遲秒數。
先用文字看時序,不急著量高度
文字內容是最容易辨認的例子。把下面程式放進自己的Vue3測試元件。頁面起始顯示0,按按鈕後,函式會分別印出資料值、立即讀取的文字,以及等待更新後的文字。
<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
const output = ref(null)
async function changeAndRead() {
count.value = 1
console.log('state', count.value)
console.log('immediate DOM', output.value.textContent)
await nextTick()
console.log('updated DOM', output.value.textContent)
}
</script>
<template>
<p ref="output">{{ count }}</p>
<button @click="changeAndRead">改值後讀畫面</button>
</template>
這個範例使用template ref取得元件自己的元素,避免用全頁query selector找錯同名元素。按鈕是在元件掛載後才能點到,因此元素已存在;如果程式可能在元素還沒建立時呼叫,就仍要處理空ref,不能把nextTick當成任何元素都保證存在。
本文真瀏覽器案例採Vue3.5.43,起始文字0,改值後資料是1、立即DOM仍是0,等待nextTick後DOM變1。這個結果比較的是同一輪函式裡的更新時序;不是說使用者畫面會固定卡住某個毫秒數,也不是每次讀取都能觀察到相同延遲。
重複把count設成已經是1的值,並不是新一次值變更。要重做比較,先重設到0並等待那次更新,或重新載入測試頁。否則看到兩次都是1,就可能誤以為第一次觀察錯了,而其實只是測試輸入沒有改變。
await放在改資料之後
nextTick()回傳Promise,可以await,也可以傳回呼。讀者若原本寫在一般函式裡,記得把函式改為async,再按順序放置;不是把它隨便放在任何一行就會有相同作用。
items.value.push({ id: 3, title: 'new item' })
await nextTick()
const last = listElement.value?.lastElementChild
last?.scrollIntoView({ block: 'nearest' })
這是清單範例的操作順序:先讓資料包含新項目,等待Vue把它渲染進DOM,再找到最後一個元素。若列表的實際順序經過排序或篩選,最後元素不一定是剛加入那筆,應按穩定ID定位;nextTick只處理時序,不替你辨認哪個元素對應哪筆資料。
同樣地,先 await nextTick() 再修改items,等待的是前一輪,不能保證後面那次修改的DOM也已完成。看到程式有nextTick就判定「已等更新」不夠,要看它與資料變更、DOM讀取三者的先後位置。
固定setTimeout不是同一種等待
有人用 setTimeout(..., 100)讓問題暫時消失。這是過了一段時間再做事,沒有直接表達你要等Vue的哪個更新。機器負載、背景分頁或操作節奏不同時,固定時間可能浪費等待,也可能沒有解決真正要等的資源。
如果只需要Vue更新DOM,就用nextTick表達這個條件,讓後來維護的人知道等待的理由。如果要等API回應,則await那個請求;如果要等圖片載入,處理圖片的載入狀態。把三者全部換成一段固定延遲,只是把不同工作塞在同一個時間猜測裡。
nextTick也不是取消請求、debounce或節流工具。搜尋輸入要控制請求頻率、避免舊回應覆蓋新回應,需要另外處理;在請求旁邊加nextTick,不會改變網路資料何時回來。本文聚焦DOM更新後的讀取,不改寫請求控制流程。
等DOM不等於所有版面資源都穩定
面板展開後,用 getBoundingClientRect()讀尺寸,可以得到當下元素相對視窗的矩形。Vue更新完成後,你讀到的是新DOM,但圖片、字型、CSS transition或其他非Vue控制的資源,還可能繼續改變尺寸。
例如新加入的圖片還沒載入,下一刻可能撐高卡片;字型替換也可能改變換行。若你的任務要求最終尺寸,就應處理那些資源的完成條件,必要時觀察元素尺寸變動。不要把「nextTick後讀到一個數字」寫成「整個版面已永遠固定」。
getBoundingClientRect的座標會受捲動位置影響,寬高也有包含邊框等條件;量測前先確定你需要的是相對視窗位置、元素盒子尺寸,還是內容高度。nextTick只讓讀取發生在Vue更新之後,不能幫你選對量測API。
watcher裡讀DOM,可考慮flush post
如果DOM讀取發生在watcher回呼裡,Vue官方文件說明預設回呼在該元件DOM更新前執行。需要讀更新後的owner DOM時,可考慮 flush: 'post',把這個需求直接寫在監聽設定上。
watch(open, () => {
const box = panel.value?.getBoundingClientRect()
console.log(box?.height)
}, { flush: 'post' })
這段需另外匯入watch,並建立open與panel;它示意的是時序選項,不是完整面板元件。選用post後是否仍有其他要等待的資源,依實際畫面決定。不要把它與nextTick到處重複堆疊,卻不說明每一次等待想解決什麼。
如果操作只會因為某個按鈕執行,在該操作函式裡先改資料、再await nextTick往往更直接;如果各處都可能改open,且每次更新後都要做同一件事,post watcher則可能較合適。兩種寫法應按觸發來源選,不需要為了一個函式建立多餘監聽。
掛載與反覆更新要分開處理
第一次需要存取元件DOM,可放在適合的mounted時機,或由已掛載的事件函式處理。資料改變後的DOM更新則是另一輪;mounted只發生在掛載階段,不會因每次count改變而再執行。
不要在updated裡無條件再改同一個會觸發更新的狀態,可能造成持續更新循環。若要讀畫面,先確認讀取不會又觸發同一段修改。每次更新都追加一筆資料,再等下一次更新,只會讓列表不斷增加。
查修時,最小比較就是「改值前文字、改值後資料、立即DOM、nextTick後DOM」四個位置。等這個流程確認,再接回尺寸、聚焦或捲動功能。這樣你能區分等待不足、元素定位錯誤、以及非Vue資源尚未完成,避免把所有畫面問題都歸給更新太慢。
若目標元素受 v-if控制,開關為false時它根本不在DOM裡。先把開關改成true,再等待更新,才有可聚焦的元素;反過來等完仍保持false,nextTick也不會替你產生它。可選串接避免空ref報錯,但沒有執行聚焦不代表任務已完成,仍應確認預期元素確實出現。
同一個async操作中,使用者也可能在等待時又關閉面板。回來讀ref時再次檢查當前狀態,避免操作已經移除的元素。這種檢查是照顧實際互動條件,與固定等待時間不同;你要確認的是現在是否仍應執行,而不是等待了多久。

評論0