在 Day03 我們用 Proxy 做了一個迷你 reactive,只要資料一改,畫面就馬上跟著更新。
但當時我有留一個伏筆:真正的 Vue 並不是這樣做的。
今天就來把這個伏筆收回來,順便認識一個很常用、但很多人不太懂的 API:nextTick。
<div id="app">
<p ref="msg">{{ count }}</p>
<button @click="add">+1</button>
</div>
<script type="module">
import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
createApp({
setup() {
const count = ref(0)
const msg = ref(null)
function add() {
count.value++
console.log('資料:', count.value)
console.log('畫面:', msg.value.textContent)
}
return { count, msg, add }
}
}).mount('#app')
</script>
點一下按鈕,console 會印出:
資料: 1
畫面: 0
資料已經變成 1 了,但畫面上讀到的還是 0。
這不是 bug,而是 Vue 故意這樣設計的。
用點餐來比喻。
你在餐廳點了一碗麵、一杯紅茶、一盤小菜,服務生不會每聽到一道就跑一次廚房,而是全部寫在點單上,等你點完再一次送過去。
Vue 更新畫面也是一樣的邏輯:
這樣做的好處是,就算你連續改了 100 次資料:
for (let i = 0; i < 100; i++) {
count.value++
}
畫面也只會更新 1 次,而不是 100 次。操作 DOM 是很耗效能的事情,能少做就少做。
這個機制叫做批次更新。
Day03 的迷你 reactive 大概長這樣:
set(target, key, value) {
target[key] = value
effect() // 一改就馬上執行
return true
}
這就像服務生每聽到一道菜就跑一次廚房。
真正的 Vue 多做了一步:不馬上執行,而是先放進一個待辦清單,晚一點再統一執行。這就是 Day03 留下的伏筆。
Vue 會把更新畫面的工作,放進 Promise.then() 裡面執行。
在 JavaScript 裡,Promise.then() 的內容會在目前的同步程式碼全部跑完之後才執行。
console.log('A')
Promise.resolve().then(() => console.log('B'))
console.log('C')
// 印出順序:A → C → B
所以 Vue 的更新流程是:
Promise.then() 裡一次更新畫面這也解釋了一開始的現象:console.log 是在第 1 步執行的,那時畫面還沒更新。
如果我們改完資料,就是想拿到更新後的畫面,該怎麼辦?
答案就是 nextTick,它的意思是:等畫面更新完,再繼續往下執行。
import { ref, nextTick } from 'vue'
async function add() {
count.value++
await nextTick()
console.log('畫面:', msg.value.textContent) // 1
}
nextTick 本身也是一個 Promise,而且它會排在 Vue 更新畫面的那個 Promise 後面,所以等到它的時候,畫面一定已經更新好了。
<button @click="startEdit">編輯</button>
<input v-if="isEditing" ref="inputRef" />
const isEditing = ref(false)
const inputRef = ref(null)
async function startEdit() {
isEditing.value = true // 讓輸入框出現
await nextTick() // 等畫面更新
inputRef.value.focus() // 輸入框已經在畫面上了
}
如果沒有 nextTick,isEditing 剛改成 true 時,輸入框還沒被建立,inputRef.value 是 null,就會報錯。
async function addMessage(text) {
messages.value.push(text)
await nextTick()
listRef.value.scrollTop = listRef.value.scrollHeight
}
新訊息要先出現在畫面上,高度才會變,這時捲動才會捲到正確的位置。
記一句話就好:
改完資料後,下一步要操作畫面上的元素,就加
await nextTick()。
例如 focus、捲動、量元素的寬高。
如果只是讀資料本身,就不需要,因為資料是立刻改好的,只有畫面是晚一點才更新。
Promise.then() 來安排「晚一點」的更新時機nextTick 可以讓我們等到畫面更新完再繼續明天會繼續往下走,我們下一篇見。