本文 AI 產出,尚未審核
Vue3 Composition API:watch() 與 watchEffect() 完全解析
簡介
在 Vue3 中,Composition API 讓我們以函式的方式組織邏輯,reactive、ref、computed 等 API 已經相當熟悉,接下來最常用的兩個監控工具——watch() 與 watchEffect(),則是處理副作用 (side‑effects) 的關鍵。
watch():精準監聽特定的響應式來源(單一變數、陣列或物件),適合需要在值變化前後取得舊值與新值的情境。watchEffect():自動追蹤在回呼函式內使用的所有響應式來源,類似computed的依賴收集,但不會回傳值,而是直接執行副作用。
掌握這兩個 API,不僅能讓資料變化時即時同步 UI、發送請求或清理資源,更能避免不必要的重新渲染與效能浪費。以下將從概念、語法、實作範例一路說到常見陷阱與最佳實踐,讓你在專案中得心應手。
核心概念
1. watch() 的基本語法
import { ref, watch } from 'vue'
const count = ref(0)
// 監聽單一 ref,回呼 receives (newVal, oldVal)
watch(count, (newVal, oldVal) => {
console.log(`count 從 ${oldVal} 變成 ${newVal}`)
})
- 第一個參數:要監聽的來源,可以是
ref、reactive、getter 函式,或是包含多個來源的陣列。 - 第二個參數:副作用回呼,預設接受
(newValue, oldValue)。 - 第三個參數(選填):
{ immediate, deep, flush }讓你控制首次執行、深層偵測與執行時機。
註:
watch只會在 變更後(micro‑task)觸發,除非設定flush: 'sync'。
2. watchEffect() 的簡潔寫法
import { ref, watchEffect } from 'vue'
const name = ref('Alice')
const greeting = ref('')
// 自動追蹤 name,name 變動時重新執行
watchEffect(() => {
greeting.value = `Hello, ${name.value}!`
console.log(greeting.value)
})
- 不需要明確宣告要監聽的來源,Vue 會在執行函式時「捕捉」所有被存取的響應式屬性。
- 回呼沒有參數,因此只能取得最新值,若需要舊值請改用
watch()。 - 預設 同步執行(
flush: 'pre'),適合更新 UI 前的副作用。
3. deep 監聽:觀測物件內部變化
import { reactive, watch } from 'vue'
const user = reactive({
profile: {
name: 'Bob',
age: 30
}
})
// 深層偵測 profile 內任意屬性變化
watch(
() => user.profile,
(newVal, oldVal) => {
console.log('profile 變更:', newVal)
},
{ deep: true }
)
- 若不加
deep:true,只會偵測 引用變更(即user.profile = {...}),不會捕捉內部屬性的修改。
4. immediate:首次執行監聽
watch(
count,
(newVal) => {
console.log('首次或之後的 count:', newVal)
},
{ immediate: true } // 立即執行一次
)
- 常用於 初始化 時需要根據當前值執行一次副作用(例如載入遠端資料)。
5. 清理函式(onInvalidate)
watch() 與 watchEffect() 都支援在每次重新執行前清理舊的副作用,常見於 防抖、取消請求 等情境。
import { ref, watchEffect } from 'vue'
const query = ref('vue')
let controller = null
watchEffect((onInvalidate) => {
// 取消上一次的請求
if (controller) controller.abort()
controller = new AbortController()
fetch(`https://api.example.com/search?q=${query.value}`, {
signal: controller.signal
})
.then(res => res.json())
.then(data => console.log(data))
// 設定清理函式
onInvalidate(() => {
controller.abort()
console.log('上一個請求已取消')
})
})
程式碼範例(實用案例)
範例 1️⃣:表單驗證的即時回饋(watchEffect)
import { ref, watchEffect } from 'vue'
export default {
setup() {
const email = ref('')
const emailError = ref('')
// 每次 email 改變即時驗證
watchEffect(() => {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
emailError.value = pattern.test(email.value) ? '' : '請輸入有效的 Email'
})
return { email, emailError }
}
}
重點:
watchEffect只要watch(email, ...)更簡潔。
範例 2️⃣:路由參數變化時重新抓取資料(watch + immediate)
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
export default {
setup() {
const route = useRoute()
const post = ref(null)
const fetchPost = async (id) => {
const res = await fetch(`https://api.example.com/posts/${id}`)
post.value = await res.json()
}
// 監聽路由參數 id,首次立即執行
watch(
() => route.params.id,
(newId) => {
fetchPost(newId)
},
{ immediate: true }
)
return { post }
}
}
範例 3️⃣:深層偵測設定物件的變更(watch + deep)
import { reactive, watch } from 'vue'
export default {
setup() {
const settings = reactive({
theme: { dark: false },
layout: { sidebar: true }
})
watch(
() => settings,
(newVal) => {
console.log('設定變更,寫入 localStorage')
localStorage.setItem('app-settings', JSON.stringify(newVal))
},
{ deep: true }
)
return { settings }
}
}
範例 4️⃣:防抖搜尋(watch + onInvalidate)
import { ref, watch } from 'vue'
export default {
setup() {
const keyword = ref('')
const results = ref([])
watch(
keyword,
(newVal, _, onInvalidate) => {
const timer = setTimeout(async () => {
const res = await fetch(`https://api.example.com/search?q=${newVal}`)
results.value = await res.json()
}, 300) // 300ms 防抖
// 若 keyword 在 300ms 內再次改變,清除計時器
onInvalidate(() => clearTimeout(timer))
}
)
return { keyword, results }
}
}
範例 5️⃣:自動取消未完成的 HTTP 請求(watchEffect + onInvalidate)
import { ref, watchEffect } from 'vue'
export default {
setup() {
const userId = ref(1)
const user = ref(null)
watchEffect((onInvalidate) => {
const controller = new AbortController()
fetch(`https://api.example.com/users/${userId.value}`, {
signal: controller.signal
})
.then(r => r.json())
.then(data => (user.value = data))
.catch(err => {
if (err.name !== 'AbortError') console.error(err)
})
// 清理:切換 userId 時取消前一次請求
onInvalidate(() => controller.abort())
})
return { userId, user }
}
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 / 實踐 |
|---|---|---|
過度使用 watchEffect |
watchEffect 會追蹤所有在回呼裡讀取的響應式值,若不慎把大量無關的邏輯放進去,會導致 不必要的重新執行。 |
只在純副作用(如 DOM 操作、API 呼叫)使用 watchEffect,對於需要舊值或精確控制的情況改用 watch。 |
忘記 deep: true |
監聽 reactive 物件的屬性時,若只監聽引用,內部屬性變更不會觸發。 |
需要觀測深層結構時,加上 { deep: true },或改寫為 分段監聽(如 watch(() => obj.prop, ...))。 |
未使用 immediate |
初始化時需要根據當前值執行一次副作用,卻忘記加 immediate,造成 UI 與資料不同步。 |
在需要「首次同步」的場景(如載入資料、設定預設值)加上 { immediate: true }。 |
| 清理函式遺漏 | 異步請求、定時器或訂閱未在重新執行前取消,會產生 記憶體洩漏 或 競爭條件。 | 使用 onInvalidate(watch)或 onInvalidate 參數(watchEffect)必寫清理邏輯。 |
flush 時機誤用 |
預設 flush: 'pre'(在 DOM 更新前),有時需要等 DOM 完成才執行(如操作實際元素),此時使用 flush: 'post'。 |
根據需求選擇 pre、post 或 sync,尤其在操作外部 UI 套件時使用 post。 |
最佳實踐小結
- 先思考是否需要舊值:若需要,使用
watch;若僅需最新值,watchEffect更簡潔。 - 盡量限制監聽範圍:使用 getter 函式或陣列,只監聽必要的屬性,避免不必要的重算。
- 配合
onInvalidate:所有涉及非同步或資源占用的副作用,都應提供清理機制。 - 適時使用
flush:對於需要在 DOM 更新前/後執行的副作用,明確指定flush,提升可預測性。 - 測試與除錯:在開發環境開啟 Vue Devtools,觀察
watch/watchEffect的觸發次數,確保不會過度重複。
實際應用場景
| 場景 | 建議使用 | 為什麼 |
|---|---|---|
| 即時表單驗證 | watchEffect |
只要表單欄位被讀取即自動重新驗證,寫法最簡潔。 |
| 路由變更時重新抓取資料 | watch + immediate |
需要監聽特定路由參數變化,並在首次載入時立即執行一次。 |
| 全局設定(主題、語系)同步到 localStorage | watch + deep |
設定是深層物件,需捕捉任意屬性變更,並持久化。 |
| 搜尋框的防抖請求 | watch + onInvalidate |
必須在每次輸入變化時取消前一次的計時器,避免過多請求。 |
| 多頁面切換時取消未完成的 API | watchEffect + onInvalidate |
每次依賴變更(如 userId)都會自動取消舊的請求,避免競爭。 |
| 動畫庫或第三方 UI 元件的同步 | watch + flush: 'post' |
需要等 Vue 完成 DOM 更新後才調用外部庫的 API。 |
總結
watch()與watchEffect()是 Vue3 Composition API 中處理 副作用 的兩把利器。watch()精準、支援舊值、深層偵測與首次立即執行,適合需要細部控制的場景。watchEffect()自動追蹤所有在回呼裡使用的響應式來源,語法最簡潔,適合即時回饋與一次性副作用。- 正確使用
deep、immediate、flush以及 清理函式 (onInvalidate) 能避免效能問題與記憶體洩漏。 - 在實務開發中,根據「是否需要舊值」與「監聽範圍」選擇合適的 API,搭配最佳實踐,就能寫出 可維護、效能佳 的 Vue3 應用。
掌握這兩個 API,你的 Vue3 專案將不再因資料變化而手忙腳亂,真正做到「資料驅動」的開發哲學。