Skip to content

Vue 3 監聽器完全指南:深入掌握 watch 與 watchEffect 的非同步副作用處理

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) => {
  /* ... */
});

總結:開發選擇建議

  • 只是要算出新值:那是 computed 的守備範圍,別用監聽器手動同步。
  • 選擇 watch:當您需要比較新舊值的變化,或者希望明確精準地控制,到底是哪一個變數改變才觸發副作用時。
  • 選擇 watchEffect:當您有多個響應式依賴變數,且希望程式碼更簡潔、在初始化時就自動執行副作用時。
  • 防範記憶體外洩:在非同步環境下建立的監聽器,請務必手動呼叫其回傳函數進行銷毀。

watch vs watchEffect 快速對比

watchwatchEffect
依賴追蹤需明確手動指定自動收集函數內的響應式變數
新、舊值比對可取得新、舊值無法取得舊值,僅關注最新狀態
首次執行不會,但可以設定 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 以上版本。

延伸閱讀