Vue 3 監聽器完全指南:深入掌握 watch 與 watchEffect 的非同步副作用處理
在 Vue 3 的響應式系統中,監聽器 是處理資料變更、執行非同步操作,例如:API 請求、定時器、localStorage 寫入與手動操作 DOM 的核心工具。
許多開發者在撰寫 Composition API 時,常在 watch 與 watchEffect 之間猶豫不決。本文將帶您深入這兩者的底層邏輯,全面掌握 Vue 3 監聽器的實戰技巧與效能優化。
精準控制的利器:Vue 3 watch 的基本用法
watch 的特點是 明確且被動。您必須指定要監聽的資料來源,但也因此能取得變更前後的新、舊值,這在需要做邏輯比對時非常有用。
監聽各種資料來源
watch 支援多種格式的監聽來源,包括 ref、reactive 物件,或是 Getter 函數:
js
const count = ref(0);
const user = reactive({ age: 25, name: "Alex" });
// 來源 A: 監聽單一 Ref
watch(count, (newVal, oldVal) => {
console.log(`計數從 ${oldVal} 變為 ${newVal}`);
});
// 來源 B: 監聽 Reactive 物件的特定屬性(必須使用 Getter 函數)
watch(
() => user.age,
(newAge, oldAge) => {
console.log(`年齡從 ${oldAge} 變更為: ${newAge}`);
}
);進階配置選項:immediate、deep 與 once
watch 預設只有在資料首度改變時才會觸發。透過第三個參數,可以精細控制其行為:
immediate: true:初始化時立即執行一次,適合用在一進入頁面就要根據當前路由 ID 取得後端資料的場景。deep: true:深度監聽物件內層。但要注意,深度監聽會遞迴遍歷整個物件,當物件結構龐大時,會造成極大的效能負擔。once: true: 只會監聽並觸發一次,觸發後該監聽器會自動關閉,適合一次性的事件綁定。版本限制
此功能必須在 3.4+ 才能運作。
js
watch(
user,
(newVal) => {
console.log("User 物件發生變動");
},
{
immediate: true,
deep: true,
once: false,
}
);自動追蹤的偵測器:watchEffect
相較於 watch 必須明確指定來源,watchEffect 則是 自動且主動。它會立即執行一次傳入的函數,並自動追蹤該函數內部使用到的所有響應式變數。只要其中任何一個變數改變,函數就會重新執行。
js
const userId = ref(1);
const page = ref(1);
// 初始化即執行一次,且自動監聽 userId 與 page 的變更
watchEffect(() => {
console.log(
`自動觸發:取得使用者 ${userId.value} 在第 ${page.value} 頁的資料`
);
});關鍵的效能防線:如何正確停止監聽?
在 Vue 3 中,當元件被銷毀時,系統會自動停止綁定在該元件上的監聽器。因此,在大多數情況下您無需關心如何停止。
但是,如果監聽器是在非同步回呼函數中建立的,它就不會綁定在當前元件上! 這會導致元件銷毀後監聽器依然留在記憶體中運行,進而引發 記憶體外洩。
js
// 它會自動停止
watchEffect(() => {});
// ...這個則不會!
setTimeout(() => {
watchEffect(() => {});
}, 100);要解決這個問題,我們必須使用 watch 或 watchEffect 宣告時回傳的停止導向函數。
手動停止 watch 監聽器
呼叫回傳的函數即可主動關閉 watch()。
js
const count = ref(0);
// 1. 用一個變數接住 watch 的回傳值
const removeWatch = watch(count, (newVal) => {
console.log(`目前數字:${newVal}`);
// 2. 設定停止條件
if (newVal >= 5) {
console.log("已達到 5,停止監聽!");
removeWatch(); // 3. 執行該函數,這條監聽流水線就此關閉
}
});手動停止 watchEffect 監聽器
同樣地,呼叫回傳的函數即可主動關閉 watchEffect()。
js
const count = ref(0);
// 1. 用一個變數接住 watchEffect 的回傳值
const removeWatchEffect = watchEffect(() => {
console.log(`目前數字:${count.value}`);
// 2. 設定停止條件
if (count.value >= 5) {
console.log("已達到 5,停止監聽!");
removeWatchEffect(); // 3. 執行該函數,這條監聽流水線就此關閉
}
});搭配生命週期銷毀非同步監聽器
若您在元件中使用了非同步監聽器,最佳實踐是搭配 onUnmounted() 一併清除:
js
const token = ref("");
let stopAsyncWatch = null;
// 元件銷毀時,確保手動切斷非同步監聽器,防止記憶體外洩
// 關鍵:必須在第一個 await 之前註冊生命週期
onUnmounted(() => {
if (stopAsyncWatch) stopAsyncWatch();
});
// 模擬在非同步行為後才建立的監聽器
await setupAsyncProcess();
stopAsyncWatch = watch(token, (newToken) => {
/* ... */
});總結:開發選擇建議
- 選擇 watch:當您需要比較新舊值的變化,或者希望明確精準地控制,到底是哪一個變數改變才觸發副作用時。
- 選擇 watchEffect:當您有多個響應式依賴變數,且希望程式碼更簡潔、在初始化時就自動執行副作用時。
- 防範記憶體外洩:在非同步環境下建立的監聽器,請務必手動呼叫其回傳函數進行銷毀。
watch vs watchEffect 快速對比
| watch | watchEffect | |
|---|---|---|
| 依賴追蹤 | 需明確手動指定 | 自動收集函數內的響應式變數 |
| 新、舊值比對 | 可取得新、舊值 | 無法取得舊值,僅關注最新狀態 |
| 首次執行 | 不會,但可以設定 immediate: true | 初始化時必定立即執行一次 |
