昨天我們接觸到watch
、watchEffect
,今天繼續來挖掘對Vue還不熟之前我的一些疑問,深入探討研究Vue監聽器,即便實務上已經使用的很熟悉,跟chatGPT答辯後才發現原來不只是官網,我也忽略掉滿多細節,造成用的時候觀念模稜兩可呢~
相信很多文章或官網已經介紹滿清楚watch監聽器
功能,主要是監聽某些響應式數據做一些副作用(side effect)
處理,像是呼叫api、更動其他響應式數據源,不過我比較好奇的是,到底watch執行的時間點是在什麼時候。
是來自以前學React官方有提及介紹的關於純粹元件(pure components)
的描述觀念:
應該避免依賴於外部的可變狀態或副作用來影響元件初次在畫面上的渲染
例如組件的渲染前執行很多API獲取資料、進行原生瀏覽器DOM API的操作,因為必須依賴於這些外部的帶進來的非響應式資料變量,這可能會導致不同步的渲染結果,就會違反這一原則。
像是API呼叫獲取資料等副作用,React的設計思維: 渲染完畫面將資料先放到瀏覽器呈現後再執行side effect,確保不阻塞影響畫面渲染。
以下張圖來說(圖片出處),我們在元件裡定義好的初始資料,應該忠實地反應在右邊輸出:
如果以Vue來假設行為也是類似的話:
一個SFC組件文件要放到網頁上渲染時,裡面的初始化或更新的邏輯,應該保持像是純函式(pure function)般行為,每次丟進去的資料源,產製出來的UI都是穩定一樣。
那我們來深入watch監聽器執行副作用時,時機點是不是也是如同pure component設計思維般純粹?
回顧一下watch基本用法:
watch
會監聽一個響應式數據來源(ref 或reactive),當監聽數據有變化時,會執行回調函式(callback function)
,同時也提供接收兩個新舊值參數(newValue、oldValue)
供你作一些額外驗證使用。
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// callback回調函式,當前面監聽數據有變化才執行
})
obj.count+
那麼watch回調函式(處理副作用)執行時機點預設運作時機又是如何,會如同React一樣保持到瀏覽器渲染完畢嗎?
瀏覽器更新前執行回調函式(預設pre模式)✅
等到最後瀏覽器更新DOM完畢後才執行回調函式(post模式)❌
我們可以另外寫一個案例來理解watch
的行為
我們使用ref
的捕捉樣板DOM元素的功能,watch 回調函式(callback function)內容去捕捉樣板DOM元素內容
。
你會發現一直抓到舊的DOM元素內容,console.log()顯示內容總是還沒更新前的DOM元素內容,看起來執行回調函式副作用的行為,會在瀏覽器渲染完成前執行完畢。
<template>
<button @click='count++' ref='target'>按我 +1 -{{ count }} </button>
</template>
<script setup>
import {ref, watch} from 'vue'
const count = ref(0)
const target = ref(null)
watch(count,(val)=>{
console.log("watch執行時機點", val, 'DOM元素內容', target.value.innerText)
})
</script>
在Vue watch 函式中允許你監視一個或多個響應式數據的變化,並在數據變更時執行特定的邏輯。比較特殊的是 Vue 3中的 watch 有一個名為 flush
的選項,這個選項可以控制監視回調的觸發時機,在瀏覽器渲染的前後:
回調函數在 Vue 進行瀏覽器 DOM 更新之前觸發,是watch的預設模式。
意味著在回調執行的時候,DOM 還沒有根據變更進行更新,所以上面案例按下按鈕捕捉到都是還沒更新的舊DOM元素囉~。
這個模式適合需要在 DOM 更新前執行邏輯的情況,例如在數據變更時執行一些同步處理或檢查。
回調函數在 Vue 進行Virtual DOM 更新之後觸發(註:但在瀏覽器渲染重繪之前執行)
需要等待 DOM 更新後再執行的操作,尤其是在依賴最新的 DOM 結構進行操作的情況下,像是必須調用瀏覽器API的滾軸行為(例如計算 DOM 元素的大小或位置,滾到最新的對應位置)。
回調函數會在數據變更的那一刻立即同步觸發。這與其他兩個模式不同,它不會等待 Vue 的下一個異步 DOM 更新循環。
這適合需要立即響應數據變化的情況,因為會高頻率觸發回調,應該謹慎使用,因為它可能會影響性能或造成一些預料外的副作用。
flush: pre
和flush: sync
看起字面上意思很像,但flush: pre
模式多了個批次更新(batch update)
處理,來看一段在Vue社群討論區有人討論的案例:
也有完整案例程式碼,可以玩玩看~
分別設計了監聽a和b響應式變數的watch監聽器,分別有3種flush: pre
、 flush: sync
和flush: sync
模式,我們在組件掛載到瀏覽器上後,調用其中的生命週期函式 onMounted 執行兩次a變數的資料更新 。
flush: pre 最終只會執行一次回調函式,並且把兩次++操作對響應式資料先集中起來,再一併更新到畫面上
,這就是所謂的批次更新(batch update),而flush: sync 會執行兩次。
<script setup>
import { ref, watch, reactive, onMounted } from 'vue';
const output = reactive({
pre: "",
sync: "",
post: "",
});
const a = ref(0);
const b = ref(0);
watch([ a, b ], () => {
output.pre += `pre, a: ${a.value}, b: ${b.value}\n`;
console.log("pre callback");
}, { flush: "pre" });
watch([ a, b ], () => {
output.sync += `sync, a: ${a.value}, b: ${b.value}\n`;
}, { flush: "sync" });
watch([ a, b ], () => {
output.post += `post, a: ${a.value}, b: ${b.value}\n`;
}, { flush: "post" });
console.log("before mutations");
onMounted(() => {
a.value++;
a.value++;
b.value++;
console.log("after mutations");
})
</script>
<template>
<pre>{{ output.pre + "\n" }}{{ output.sync + "\n" }}{{ output.post + "\n" }}</pre>
</template>
其實Vue的批次更新算是一種收集數據變化,和數據相關的副作用一起收集處理的機制
,會透過瀏覽器提供的環境,來達到非同步的處理更新,會跟JavaScript 事件循環
運作有關,稍微提一下宏觀概念,之後文章有機會作深入介紹:
JavaScript 的事件循環機制決定了同步程式碼會優先執行,非同步程式碼則會延後執行。Vue 正是利用這個特性,在同一個事件循環內,將多次資料變動合併處理。
Vue 的響應式系統會追蹤資料(Data)與畫面(View)的依賴關係,當資料變動時,變動並不會立即觸發 DOM 更新,而是先將變動記錄在一個排程佇列中(scheduler)
。
等到當前事件循環的同步程式碼執行完畢後,Vue 才會統一處理這些變動,最終更新 DOM
。
這樣的設計可以避免多次不必要的 DOM 操作,提升效能。
轉化成流程會有幾個步驟:
好像沒有人試著把它畫成比較簡易的圖理解,大概就找到這篇文章的圖,會比較對上面這段話更有感覺~
Vue watch 執行時機點預設是真實DOM更新前完成執行 ,跟React的useEffect設計上不太一樣
,也有多種執行時機點和模式可以依需求選擇。
watch預設有批次更新(batch update)處理,將多次數據變動收在一起
,在瀏覽器runtime執行時,會在渲染週期內,透過JS事件循環排入主線程式碼中執行,後續再針對瀏覽器DOM更新
、render queue渲染圖層
更新等。
發現好多JavaSctipt舊觀念Event Loop
、Event queue
、微任務(MicroTask)和渲染週期(Render Cycle)
等這些觀念,在框架中也會出現呢,有興趣深入了解的話可以用這些關鍵字去作搜尋學習~