Skip to content

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 快速對比

watchwatchEffect
依賴追蹤需明確手動指定自動收集函數內的響應式變數
新、舊值比對可取得新、舊值無法取得舊值,僅關注最新狀態
首次執行不會,但可以設定 immediate: true初始化時必定立即執行一次