本文 AI 產出,尚未審核

Vue3 Composition API(核心) – 非同步 watch 處理

簡介

在 Vue 3 中,watch 是觀察 reactive 狀態變化的關鍵工具。大多數教學只示範同步回呼,然而實務開發常會遇到 API 請求、計時器、debounce 等非同步操作。如果不正確處理,容易出現競爭條件、記憶體泄漏或 UI 異步更新錯亂的問題。
本篇文章將深入探討在 Composition API 中如何安全、有效地使用非同步 watch,從基礎概念到實作技巧,讓你在面對資料驅動的非同步需求時,能寫出可讀、可維護的程式碼。

核心概念

1. watch 的基本語法

import { ref, watch } from 'vue'

const count = ref(0)

watch(count, (newVal, oldVal) => {
  console.log(`count 從 ${oldVal} 變成 ${newVal}`)
})

watch 接收兩個主要參數:要觀察的 source(單一 ref、reactive 或 getter)以及回呼函式。回呼本身可以是同步或 返回 Promise 的非同步函式。

2. 為什麼要使用非同步 watch

  • API 請求:使用者輸入搜尋關鍵字後,需要向後端取得結果。
  • 防抖 / 節流:避免在短時間內頻繁觸發昂貴計算。
  • 延遲執行:例如在切換路由後等待動畫結束再載入資料。

3. watch 支援 async 回呼

Vue 允許 watch 的回呼直接寫成 async,框架會自動捕捉返回的 Promise,並在它被 reject 時在開發環境印出警告。

watch(
  () => query.value,
  async (newQuery) => {
    const res = await fetch(`/api/search?q=${newQuery}`)
    results.value = await res.json()
  },
  { immediate: true }
)

注意:watch 本身不會等到 Promise 完成才結束監聽,若在回呼內部有多次觸發,舊的請求仍會跑完,可能導致資料被舊請求覆蓋。

4. 取消舊的非同步任務(防止競爭條件)

4.1 使用 AbortController

import { ref, watch } from 'vue'

const query = ref('')
const results = ref([])
let controller = null

watch(
  query,
  async (newQ) => {
    // 取消前一次的請求
    if (controller) controller.abort()
    controller = new AbortController()

    try {
      const res = await fetch(`/api/search?q=${newQ}`, {
        signal: controller.signal
      })
      results.value = await res.json()
    } catch (e) {
      if (e.name !== 'AbortError') console.error(e)
    }
  }
)

AbortController 讓我們在新一次觸發前 主動終止 仍在執行的請求,確保 UI 只顯示最新的結果。

4.2 使用計數器(版本號)

let version = 0

watch(
  query,
  async (newQ) => {
    const cur = ++version   // 為本次請求打上唯一版本號
    const res = await fetch(`/api/search?q=${newQ}`)
    const data = await res.json()
    // 只在版本號仍是最新時才更新
    if (cur === version) results.value = data
  }
)

藉由比對 版本號,即使舊請求仍回傳,也不會覆蓋較新的資料。

5. flush 選項與非同步更新

watch 的 flush 設定決定回呼何時執行,預設是 pre(在 DOM 更新前)。對於需要 等到畫面渲染完成 後才執行的非同步工作,應改為 post:

watch(
  () => props.visible,
  async (show) => {
    if (show) {
      await nextTick()          // 等待 DOM 完成渲染
      await animateIn()         // 執行進場動畫
    }
  },
  { flush: 'post' }
)

6. 多個來源的非同步 watch

watch(
  [searchTerm, filterOptions],
  async ([term, filter]) => {
    const res = await fetch(`/api/items?term=${term}&filter=${filter}`)
    items.value = await res.json()
  },
  { immediate: true }
)

使用陣列作為 source 時,回呼會一次收到所有新值。務必在回呼內自行處理取消或版本控制,避免同時觸發多個請求。

常見陷阱與最佳實踐

陷阱 可能的後果 解決方案
忘記取消舊請求 產生競爭條件,舊資料覆蓋新資料 使用 AbortController 或版本號
在 watch 內直接改變被監看的值 造成無限迴圈或不必要的重渲染 只在需要時使用 nextTick,或分離副作用到另一個 watch
忽略 flush 設定 非同步任務在 DOM 尚未更新前執行,導致動畫或測量錯誤 依需求選擇 pre、post、sync
未處理 Promise 錯誤 開發環境會噴出未捕獲的錯誤訊息 使用 try/catch 或 .catch() 捕獲
過度監聽大型物件 性能下降 使用 getter 只返回必要的子屬性,或使用 deep: false

最佳實踐

  1. 始終為非同步 watch 加上錯誤處理。
  2. 在需要時加入防抖 / 節流(lodash.debounce、watchEffect 搭配 setTimeout)。
  3. 盡量把副作用抽成可重用的 composable,例如 useFetch、useDebouncedRef。
  4. 測試競爭條件:在開發工具的 Network 面板觀察同時發出的請求,確保只有最新的回應被使用。

實際應用場景

場景 1:即時搜尋建議

使用者在輸入框輸入文字,透過 watch 監聽 searchTerm,每次變更都呼叫建議 API。加入 防抖 與 取消舊請求,避免每個鍵入都觸發網路請求。

import { ref, watch } from 'vue'
import debounce from 'lodash.debounce'

const searchTerm = ref('')
const suggestions = ref([])
let abortCtrl = null

watch(
  searchTerm,
  debounce(async (term) => {
    if (abortCtrl) abortCtrl.abort()
    abortCtrl = new AbortController()
    const res = await fetch(`/api/suggest?q=${term}`, {
      signal: abortCtrl.signal
    })
    suggestions.value = await res.json()
  }, 300),
  { immediate: true }
)

場景 2:分頁資料自動載入

當 page 改變時,根據新頁碼向後端請求資料。若使用者快速點擊「下一頁」按鈕,版本號 可確保舊頁面的回應不會覆蓋新頁面的資料。

const page = ref(1)
const items = ref([])
let version = 0

watch(page, async (newPage) => {
  const cur = ++version
  const res = await fetch(`/api/items?page=${newPage}`)
  const data = await res.json()
  if (cur === version) items.value = data
})

場景 3:路由變化後的延遲資料載入

在切換路由至「詳細頁」時,需要先完成過渡動畫,再載入詳細資料。利用 watch 監聽 route.params.id,配合 flush: 'post' 與 nextTick,確保動畫先結束。

import { watch, nextTick } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()
const detail = ref(null)

watch(
  () => route.params.id,
  async (id) => {
    await nextTick()          // 等待 DOM 更新(過渡動畫已開始)
    const res = await fetch(`/api/item/${id}`)
    detail.value = await res.json()
  },
  { flush: 'post', immediate: true }
)

總結

非同步 watch 是 Vue 3 中處理資料驅動、需要與外部資源互動時的關鍵利器。掌握以下要點,就能寫出 可靠、效能佳、易維護 的程式碼:

  1. 使用 async 回呼,並加入 try/catch 捕捉錯誤。
  2. 主動取消舊任務(AbortController、版本號)以防止競爭條件。
  3. 根據需求調整 flush,確保非同步工作在正確的生命週期階段執行。
  4. 結合防抖、節流與可重用 composable,提升開發效率與程式品質。

透過本文提供的概念與範例,你現在可以在 Vue 3 的 Composition API 中,安全地使用非同步 watch,無論是即時搜尋、分頁載入或是路由切換後的延遲請求,都能輕鬆應對。祝你開發順利,寫出更好的 Vue 應用!