Vue 3 監聽器完全指南:深入掌握 watch 與 watchEffect 的非同步副作用處理
在 Vue 3 的 響應式系統 中,監聽器 是處理資料變更、執行非同步操作,例如:API 請求、定時器、localStorage 寫入與手動操作 DOM 的核心工具。
許多開發者在撰寫 Composition API 時,常在 watch 與 watchEffect 之間猶豫不決。本文將帶您深入這兩者的底層邏輯,全面掌握 Vue 3 監聽器的實戰技巧與效能優化。
精準控制的利器:Vue 3 watch 的基本用法
watch 的特點是 明確且被動。您必須指定要監聽的資料來源,但也因此能取得變更前後的新、舊值,這在需要做邏輯比對時非常有用。
監聽各種資料來源
watch 支援多種格式的監聽來源,包括 ref、reactive 物件,或是 Getter 函數:
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+ 才能運作。
watch(
user,
(newVal) => {
console.log('User 物件發生變動');
},
{
immediate: true,
deep: true,
once: false,
}
);自動追蹤的偵測器:watchEffect
相較於 watch 必須明確指定來源,watchEffect 則是 自動且主動。它會立即執行一次傳入的函數,並自動追蹤該函數內部使用到的所有響應式變數。只要其中任何一個變數改變,函數就會重新執行。
const userId = ref(1);
const page = ref(1);
// 初始化即執行一次,且自動監聽 userId 與 page 的變更
watchEffect(() => {
console.log(
`自動觸發:取得使用者 ${userId.value} 在第 ${page.value} 頁的資料`
);
});關鍵的效能防線:如何正確停止監聽?
在 Vue 3 中,當元件被銷毀時,系統會自動停止綁定在該元件上的監聽器。因此,在大多數情況下您無需關心如何停止。
但是,如果監聽器是在非同步回呼函數中建立的,它就不會綁定在當前元件上! 這會導致元件銷毀後監聽器依然留在記憶體中運行,進而引發 記憶體外洩。
// 它會自動停止
watchEffect(() => {});
// ...這個則不會!
setTimeout(() => {
watchEffect(() => {});
}, 100);要解決這個問題,我們必須使用 watch 或 watchEffect 宣告時回傳的停止導向函數。
手動停止 watch 監聽器
呼叫回傳的函數即可主動關閉 watch()。
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()。
const count = ref(0);
// 1. 用一個變數接住 watchEffect 的回傳值
const removeWatchEffect = watchEffect(() => {
console.log(`目前數字:${count.value}`);
// 2. 設定停止條件
if (count.value >= 5) {
console.log('已達到 5,停止監聽!');
removeWatchEffect(); // 3. 執行該函數,這條監聽流水線就此關閉
}
});搭配生命週期銷毀非同步監聽器
若您在元件中使用了非同步監聽器,最佳實踐是搭配 onUnmounted() 一併清除:
const token = ref('');
let stopAsyncWatch = null;
// 元件銷毀時,確保手動切斷非同步監聽器,防止記憶體外洩
// 關鍵:必須在第一個 await 之前註冊生命週期
onUnmounted(() => {
if (stopAsyncWatch) stopAsyncWatch();
});
// 模擬在非同步行為後才建立的監聽器
await setupAsyncProcess();
stopAsyncWatch = watch(token, (newToken) => {
/* ... */
});總結:開發選擇建議
- 只是要算出新值:那是 computed 的守備範圍,別用監聽器手動同步。
- 選擇 watch:當您需要比較新舊值的變化,或者希望明確精準地控制,到底是哪一個變數改變才觸發副作用時。
- 選擇 watchEffect:當您有多個響應式依賴變數,且希望程式碼更簡潔、在初始化時就自動執行副作用時。
- 防範記憶體外洩:在非同步環境下建立的監聽器,請務必手動呼叫其回傳函數進行銷毀。
watch vs watchEffect 快速對比
| watch | watchEffect | |
|---|---|---|
| 依賴追蹤 | 需明確手動指定 | 自動收集函數內的響應式變數 |
| 新、舊值比對 | 可取得新、舊值 | 無法取得舊值,僅關注最新狀態 |
| 首次執行 | 不會,但可以設定 immediate: true | 初始化時必定立即執行一次 |
常見問題
watch 與 watchEffect 有什麼差別?
watch 必須明確指定監聽來源,可以拿到新舊值,預設不會立刻執行;watchEffect 會立即執行一次並自動追蹤函式內用到的響應式資料,但拿不到舊值。
為什麼監聽 reactive 物件的屬性沒有反應?
直接寫 watch(user.age, ...) 傳進去的只是當下的值。要監聽物件的特定屬性,必須改用 getter 函式,寫成 watch(() => user.age, ...)。
deep: true 會有效能問題嗎?
會。深度監聽需要遞迴遍歷整個物件建立依賴,物件越大負擔越重。若只在意某幾個欄位,建議改用 getter 精準監聽那些屬性。
監聽器需要手動停止嗎?
在元件的 setup 中同步建立的監聽器,會隨元件卸載自動停止。但在非同步回呼中建立的監聽器不會綁定到元件上,必須自行呼叫回傳的停止函式,否則會造成記憶體外洩。
watch 與 computed 該怎麼選?
只是要算出一個新值拿來顯示,用 computed;資料變動後要執行動作,例如:發送 API 請求、寫入儲存空間、操作 DOM,才用 watch。
immediate 與 once 可以一起用嗎?
可以,但 once 會在第一次觸發後就停止監聽,若同時開啟 immediate,初始化的那一次執行就是唯一的一次。once 需要 Vue 3.4 以上版本。