本文 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 選項與非同步更新

watchflush 設定決定回呼何時執行,預設是 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 尚未更新前執行,導致動畫或測量錯誤 依需求選擇 prepostsync
未處理 Promise 錯誤 開發環境會噴出未捕獲的錯誤訊息 使用 try/catch.catch() 捕獲
過度監聽大型物件 性能下降 使用 getter 只返回必要的子屬性,或使用 deep: false

最佳實踐

  1. 始終為非同步 watch 加上錯誤處理
  2. 在需要時加入防抖 / 節流lodash.debouncewatchEffect 搭配 setTimeout)。
  3. 盡量把副作用抽成可重用的 composable,例如 useFetchuseDebouncedRef
  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 應用!