const message = ref("第 12 天,還行嗎?");
setTimeout(() => {
message.value = "睏到不行";
}, 1000);
我們修改資料的步驟,就是流程圖中的紅色區塊(component reactive state/元件的響應狀態),剩下的事情由 Vue 處理。
所以,基本上,開發者在 Vue 框架下開發,可以專注處理資料的變化和邏輯,不需要自己操作 DOM,也就不太會用到一堆 document.querySelector
等。
那 DOM 的事情都改由 Vue 處理了,那如果在 DOM 變化的過程前後 (這裡泛指建立、更新、銷毀),我們想要做事怎麼辦?
這就是為什麼會提供 nextTick()
和生命週期鉤子(Hook)。
今天只會講到 nextTick()
,而他被呼叫的時間就在 Vue 幫我們更新 DOM 的時間點後,所以我們要先認識 Vue 什麼時候更新 DOM。
Vue 什麼時候更新 DOM
響應式資料更新後,Vue 會先同步更新相依數據,再以非同步的方式去更新 DOM。
這樣的好處是可以節省效能,如果開發者短時間內修改了好幾次的資料,其實 Vue 只需要渲染最終的結果,就能省去中間一直重新渲染 DOM 的效能。
数据的修改,同时会触发dom渲染,即执行一个同步修改数据任务,再执行一个dom渲染的异步任务,此时完成了一轮loop,如果此时去获取dom,那必然会在异步任务执行前获取,当然获取不到,想要获取更新后的dom,必须再一次开启一个时间循环,即使用nexttick。
所以說,更新渲染 DOM 這件事是非同步的,等於:不知道 DOM 什麼時候會完成更新
那如果你修改完資料之後,要拿新的 DOM 元素內狀態 (如:寬、高) 來做事怎麼辦?
所以 Vue 提供了 nextTick
這個 API,他被呼叫的時機,會是在 DOM 更新渲染完成後。
nextTick
呼叫時機:數據更新後,DOM 非同步更新也完成後
會回傳 promise
可以把 callback 傳進去
也可以在 async function 裡面 await nextTick()
,等 nextTick()
被呼叫後,再接著處理
可以拿到資料更新後,完成更新的 DOM 狀態(如:寬、高)來做邏輯處理
欸所以那個 tick 到底是什麼?
其實就是所謂的「事件循環」
每次修改資料後,Vue 要幫忙做一堆工作,至少包括:
(同步) 修改資料
(非同步) 修改 DOM >>> 等這個 tick 跑完,再執行 nextTick
(非同步) 執行 nextTick callback
的確是蠻少用到的,除非需要拿到資料更新後的 DOM 狀態來做事,我一時之間也想不到什麼情境,在重新認識 Vue.js 裡有提到滾動的例子。
有一個有高度限制的 <div>
在裡面用 v-for 渲染 messages 清單,每次 <input>
內容送出後,會將輸入內容 push 到 messages 陣列中,內部會新增一個新的 message 清單項目 。
我們想要在每次新增後,都將 <div>
捲到最底部,才可以看到最新的輸入內容。
期望效果:
<div class="messages" ref="messagesDiv">
<div v-for="message in messages" :key="message">{{ message }}</div>
<input
type="text"
v-model.trim="newMessage"
@keydown.enter="addToMessages"
function addToMessages() {
messages.value.push(newMessage.value);
const messagesDiv = document.querySelector(".messages");
messagesDiv.scrollTop = messagesDiv.scrollHeight;
上面這段程式碼的呈現結果:
仔細觀察程式碼的畫面,會發現每次「捲到底部」都是停在上一次新增的內容地方,因為在這個事件迴圈中,拿到的 messagesDiv.scrollHeight
還是前一次的高度。
將 callback 函式直接寫在 nextTick()
裡
function addToMessages() {
messages.value.push(newMessage.value);
nextTick(() => {
const messagesDiv = document.querySelector(".messages");
messagesDiv.scrollTop = messagesDiv.scrollHeight;
將 callback 函式直接寫在 await nextTick()
後面,等到接到 nextTick
完成再繼續執行
async function addToMessages() {
messages.value.push(newMessage.value);
await nextTick();
const messagesDiv = document.querySelector(".messages");
messagesDiv.scrollTop = messagesDiv.scrollHeight;
使用到 nextTick 通常是為了取 DOM 元素,那在 Vue 3 要怎麼拿 DOM 元素比較好?
取 DOM:搭配 template ref
Vue 在 <tamplate>
提供了特殊的 ref 屬性,透過 ref 可以直接拿到渲染後的 DOM 元素的參照(reference)。
只要先在 <script>
中宣告和 <template>
中 ref 屬性同名的變數,就可以透過這個變數拿到 DOM。
import { ref } from 'vue'
const messages = ref(['牛肉湯', '肉燥飯', '鍋燒意麵', '白糖粿'])
const newMessage = ref('')
const messagesDiv = ref(null)
async function addToMessages() {
messages.value.push(newMessage.value)
await nextTick();
//在這裡取到的 messagesDiv 已經變成 DOM 元素了
messagesDiv.value.scrollTop = messagesDiv.value.scrollHeight
注意要在元素渲染、掛載到 DOM 上之後,才能透過 ref
屬性取到該元件;所以一開始的變數(messagesDiv
)才會綁定 null
,因為 Vue 在讀取這段程式碼時,還在準備渲染,這時候還選不到 messagesDiv
這個元素。
平常很少需要把更新後的 DOM 狀態拿出來做邏輯處理,比較常見的情況,應該是瀑布流或是輪播圖,要在 API 拿資料回來後,根據新的 DOM 去計算位置等等。
但因為時間太趕,沒有找到相對應的範例,如果之後有踩到坑,會再補充。
今天的內容有點雜,整理重點如下:
Vue 是資料驅動,通常都由 Vue 幫我們處理跟 DOM 相關的事情,如:掛載、更新,想在這些階段處理事情,就需要生命週期鉤子或 nextTick()
。
響應式資料更新後,Vue 會先同步更新相依數據,再以非同步的方式去更新 DOM。
nextTick()
會在 DOM 非同步更新完成後被呼叫,在這個呼叫時機點,可以拿資料更新後的 DOM 狀態來做事
取得 DOM 元素可以透過 Vue 提供的 ref
屬性,注意要在元素掛載上去後才能拿到。
Vue Doc
Reactivity Fundamentals
Template Refs
nextTick()