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 |
最佳實踐
- 始終為非同步
watch加上錯誤處理。 - 在需要時加入防抖 / 節流(
lodash.debounce、watchEffect搭配setTimeout)。 - 盡量把副作用抽成可重用的 composable,例如
useFetch、useDebouncedRef。 - 測試競爭條件:在開發工具的 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 中處理資料驅動、需要與外部資源互動時的關鍵利器。掌握以下要點,就能寫出 可靠、效能佳、易維護 的程式碼:
- 使用
async回呼,並加入try/catch捕捉錯誤。 - 主動取消舊任務(
AbortController、版本號)以防止競爭條件。 - 根據需求調整
flush,確保非同步工作在正確的生命週期階段執行。 - 結合防抖、節流與可重用 composable,提升開發效率與程式品質。
透過本文提供的概念與範例,你現在可以在 Vue 3 的 Composition API 中,安全地使用非同步 watch,無論是即時搜尋、分頁載入或是路由切換後的延遲請求,都能輕鬆應對。祝你開發順利,寫出更好的 Vue 應用!