本文 AI 產出,尚未審核

Vue3 Options API(傳統寫法)— watch 完全指南

簡介

在 Vue3 中,Options API 仍然是許多既有專案與新手入門的首選寫法。除了 datacomputedmethodswatch(監聽器)是讓我們在資料變化時執行副作用的強大工具。無論是表單驗證、API 請求、或是同步 UI 與瀏覽器 URL,適當使用 watch 能讓程式碼更具可讀性與可維護性。

本篇文章將從 概念實作範例常見陷阱最佳實踐 以及 實務應用場景 逐層剖析,讓你在 Vue3 Options API 中自信地使用 watch,從初學者一路成長為中級開發者。


核心概念

1. watch 的定位

watch 用來觀察一個或多個響應式來源(datapropscomputed),當來源的值改變時觸發回呼函式。它不同於 computed 的「依賴追蹤」——computed 只在需要時重新計算並回傳值,而 watch 執行副作用(side‑effects),如發送 AJAX、操作 DOM、或是更新其他狀態。

2. 基本語法(Options API)

export default {
  data() {
    return {
      count: 0,
      query: '',
    };
  },
  watch: {
    // 監聽單一屬性
    count(newVal, oldVal) {
      console.log(`count 從 ${oldVal} 變成 ${newVal}`);
    },

    // 監聽多個屬性(寫法 1:使用字串陣列)
    // query: ['handler1', 'handler2'] // 這裡僅示意,可自行定義

    // 監聽多個屬性(寫法 2:使用函式返回值)
    fullInfo: {
      handler(newVal, oldVal) {
        console.log('fullInfo 改變', newVal);
      },
      deep: true,          // 深度監聽(適用於物件/陣列)
      immediate: true,    // 初始化時立即執行一次
    },
  },

  computed: {
    fullInfo() {
      return `${this.count} - ${this.query}`;
    },
  },
};
  • handler:必須接受 newValoldVal 兩個參數。
  • deep:若監聽的目標是 物件或陣列,設定 deep: true 才能捕捉巢狀屬性的變化。
  • immediate:在組件掛載後立即執行一次,常用於在初始化階段就需要根據資料執行副作用的情況。

3. 監聽多個來源

3.1 同時監聽多個屬性(陣列寫法)

watch: {
  // 只要 count 或 query 任一變動,都會呼叫同一個 handler
  ['count', 'query'](newVals, oldVals) {
    console.log('count 或 query 改變', newVals, oldVals);
  },
}

3.2 監聽計算屬性

計算屬性本身不會直接觸發 watch,但因為它是基於其他響應式來源,當這些來源變更時,watch 仍會收到新值。

computed: {
  fullInfo() {
    return `${this.count} - ${this.query}`;
  },
},
watch: {
  fullInfo(newVal) {
    console.log('fullInfo 變更為', newVal);
  },
}

程式碼範例

以下提供 5 個實用範例,涵蓋常見需求與進階技巧。每段程式碼皆附上說明註解,方便直接套用於專案。

範例 1️⃣:表單輸入即時驗證

export default {
  data() {
    return {
      email: '',
      emailError: '',
    };
  },
  watch: {
    // 每次 email 改變即時驗證
    email(newVal) {
      const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      this.emailError = emailPattern.test(newVal)
        ? ''                 // 合格則清除錯誤訊息
        : '請輸入有效的 Email';
    },
  },
};

重點watch 讓驗證邏輯與 UI 狀態分離,避免在 template 中寫過多的條件式。

範例 2️⃣:API 請求防抖(Debounce)

import { debounce } from 'lodash';

export default {
  data() {
    return {
      searchKeyword: '',
      results: [],
    };
  },
  created() {
    // 使用 lodash 的 debounce,避免使用者每次鍵入都發送請求
    this.debouncedFetch = debounce(this.fetchResults, 500);
  },
  watch: {
    searchKeyword() {
      // 只要關鍵字變動,就觸發防抖函式
      this.debouncedFetch();
    },
  },
  methods: {
    async fetchResults() {
      if (!this.searchKeyword.trim()) {
        this.results = [];
        return;
      }
      try {
        const res = await fetch(`/api/search?q=${encodeURIComponent(this.searchKeyword)}`);
        this.results = await res.json();
      } catch (e) {
        console.error('搜尋失敗', e);
      }
    },
  },
};

技巧:在 createdsetup 中先產生防抖函式,再於 watch 中直接呼叫,保持程式碼乾淨。

範例 3️⃣:深度監聽物件(表格編輯)

export default {
  data() {
    return {
      tableData: [
        { id: 1, name: 'Alice', score: 85 },
        { id: 2, name: 'Bob', score: 92 },
      ],
    };
  },
  watch: {
    // 深度監聽 tableData 中任何欄位的變化
    tableData: {
      handler(newVal, oldVal) {
        console.log('表格資料變更', newVal);
        // 例如:自動儲存至後端
        this.autoSave(newVal);
      },
      deep: true, // 必須設定才能捕捉巢狀屬性變化
      immediate: false,
    },
  },
  methods: {
    async autoSave(data) {
      // 假設有 save API
      await fetch('/api/save', {
        method: 'POST',
        body: JSON.stringify(data),
        headers: { 'Content-Type': 'application/json' },
      });
    },
  },
};

注意:深度監聽會在每次巢狀屬性變動時觸發,若資料量龐大,建議搭配 節流(throttle)或 防抖 以減少不必要的呼叫。

範例 4️⃣:路由參數變化同步資料

import { useRoute, useRouter } from 'vue-router';

export default {
  data() {
    return {
      page: 1,
      items: [],
    };
  },
  watch: {
    // 當路由 query 中的 page 變動時,自動載入新頁面的資料
    '$route.query.page': {
      handler(newPage) {
        this.page = Number(newPage) || 1;
        this.fetchPage(this.page);
      },
      immediate: true, // 首次載入時即呼叫一次
    },
  },
  methods: {
    async fetchPage(page) {
      const res = await fetch(`/api/items?page=${page}`);
      this.items = await res.json();
    },
  },
};

說明:使用字串路徑('$route.query.page')直接監聽路由變化,讓 UI 與 URL 永遠保持同步。

範例 5️⃣:跨組件共享狀態(Vuex)

import { mapState } from 'vuex';

export default {
  computed: {
    ...mapState(['authToken']),
  },
  watch: {
    // 當全局的 authToken 變化時,更新本地的 API 客戶端
    authToken: {
      handler(newToken) {
        // 假設有全局的 apiClient
        this.$root.apiClient.setToken(newToken);
      },
      immediate: true,
    },
  },
};

重點watch 能幫助我們在 全局狀態變動 時,立即調整本地或第三方套件的設定,保持應用一致性。


常見陷阱與最佳實踐

陷阱 說明 解決方式
深度監聽導致效能問題 deep: true 會在每次巢狀屬性變動時觸發,若資料結構龐大會頻繁呼叫 handler 只對必要的屬性使用深度監聽,或在 handler 中加入 防抖 / 節流
忘記 immediate 有時需要在組件掛載時立即執行一次副作用(例如載入資料),若忘記設定會導致 UI 初始狀態錯誤。 根據需求加上 immediate: true,或在 mounted 中手動呼叫一次同樣的函式。
監聽錯誤的路徑 使用字串路徑(如 '$route.query.page')時,若拼寫錯誤或路由尚未初始化,watch 不會觸發。 確認路由已正確注入,或以 computed 包裝後再監聽。
watch 中直接改變被監聽的值 若在 watch 的 handler 內修改同一個被監聽的屬性,可能造成無限迴圈。 使用 非同步(如 nextTick)或在 handler 內判斷變化是否真的需要更新。
過度使用 watch 有時可以使用 computed 直接取得衍生值,避免不必要的副作用。 先思考是否真的需要副作用(API 請求、DOM 操作),若僅是計算衍生值,改用 computed

最佳實踐

  1. 只在需要副作用時使用 watch,衍生值使用 computed
  2. 將副作用抽離到 methods,保持 watch 內部簡潔。
  3. 使用 immediate 配合 nextTick,確保在 DOM 更新完成後執行。
  4. 對大型資料結構加防抖/節流,避免頻繁觸發 API。
  5. 統一錯誤處理:在 watch 中的非同步操作,建議使用 try/catch 或全局錯誤攔截。

實際應用場景

  1. 即時搜尋(Autocomplete)
    透過 watch 監聽使用者輸入,結合防抖與遠端 API,提供流暢的自動完成體驗。

  2. 表單驗證與錯誤提示
    當使用者修改欄位時即時檢查格式,將錯誤訊息寫入對應的錯誤屬性,讓 UI 自動顯示。

  3. 與 URL 同步的分頁、過濾、排序
    監聽 $route.query 中的分頁、過濾條件,變更時自動發送請求並更新畫面。

  4. 全局認證狀態變更
    當使用 Vuex 或 Pinia 管理的 token 改變時,利用 watch 更新 HTTP 客戶端的授權標頭。

  5. 資料同步至本機儲存(LocalStorage / IndexedDB)
    監聽重要的狀態,變更時即時寫入本機儲存,實現離線支援或狀態持久化。


總結

watch 是 Vue3 Options API 中不可或缺的 副作用監聽器。透過正確的 語法handlerdeepimmediate)與 實務技巧(防抖、節流、錯誤處理),我們可以:

  • 即時回應資料變化,如表單驗證、搜尋建議。
  • 同步 UI 與外部資源,包括 API 請求、路由參數、全局狀態。
  • 保持程式碼可讀與可維護,將副作用抽離至 methods,避免在 template 中寫雜湊邏輯。

在開發過程中,請謹記只在需要副作用時使用 watch,其餘情況盡量使用 computed 取得衍生值。合理運用 watch,不僅能提升開發效率,也能讓你的 Vue3 應用在效能與使用者體驗上更上一層樓。祝你玩得開心,寫出更乾淨、更強大的 Vue3 程式!