Vue3 課程 – 響應式系統(Reactivity System)
主題:watch vs watchEffect 差別
簡介
在 Vue 3 中,響應式系統是框架的核心,也是開發者能夠以宣告式方式描述 UI 與資料關係的關鍵。當資料變動時,Vue 會自動觸發相應的更新,讓我們不必手動操作 DOM。
在實作上,Vue 提供了兩種「監聽」機制:watch 與 watchEffect。雖然它們看起來功能相近,但 設計目的、使用時機與行為細節都有所不同。了解兩者的差別,能讓你在開發過程中選擇最合適的工具,避免不必要的效能浪費或錯誤行為。
本文將從概念、語法、實作細節出發,透過多個實用範例說明 watch 與 watchEffect 的差異,並提供常見陷阱、最佳實踐與實務應用場景,幫助你在 Vue 3 中更得心應手地使用響應式監聽。
核心概念
1. watch:精準監聽特定來源
watch 允許你明確指定要監聽的響應式來源(單一值、計算屬性或是 getter 函式),並在來源變化時執行回呼函式。它的特點包括:
| 特性 | 說明 |
|---|---|
| 來源明確 | 必須傳入要觀察的 ref、reactive 屬性或 getter 函式。 |
| 延遲執行 | 只有當來源真正變化時才觸發,且預設在「微任務」結束後執行(可透過 flush: 'post' 改變時機)。 |
| 可取得舊值/新值 | 回呼函式的參數為 (newValue, oldValue, onCleanup),方便比對差異或執行清理工作。 |
| 支援深度監聽 | 透過 deep: true 可以遞迴觀察物件內層屬性變化。 |
| 可返回清理函式 | onCleanup 允許在下一次執行前先釋放資源(如取消訂閱、關閉連線)。 |
範例 1:監聽單一 ref
import { ref, watch } from 'vue'
const count = ref(0)
// 只在 count 改變時觸發
watch(count, (newVal, oldVal) => {
console.log(`count 從 ${oldVal} 變成 ${newVal}`)
})
// 測試變更
count.value++ // console: count 從 0 變成 1
範例 2:監聽多個來源(陣列寫法)
import { ref, watch } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`姓名從 ${oldFirst} ${oldLast} 改成 ${newFirst} ${newLast}`)
})
// 改變任一欄位皆會觸發
lastName.value = 'Smith' // console: 姓名從 John Doe 改成 John Smith
範例 3:深度監聽 reactive 物件
import { reactive, watch } from 'vue'
const profile = reactive({
name: 'Alice',
address: {
city: 'Taipei',
zip: '100'
}
})
// deep: true 讓內層屬性變化也能被偵測
watch(
() => profile,
(newProfile, oldProfile) => {
console.log('profile 變更', newProfile)
},
{ deep: true }
)
// 變更內層屬性即觸發
profile.address.city = 'New Taipei' // console: profile 變更 {...}
2. watchEffect:自動收集依賴、立即執行
watchEffect 採用 「副作用函式」(effect function)的概念:在函式內部直接使用響應式資料,Vue 會自動追蹤所有被讀取的依賴,並在任一依賴變化時重新執行整個函式。其特點:
| 特性 | 說明 |
|---|---|
| 自動依賴收集 | 不需要顯式指定來源,只要在函式裡使用了響應式值,就會被追蹤。 |
| 立即執行 | 副作用函式在建立時會立即執行一次(相當於 immediate: true)。 |
| 無新舊值參數 | 回呼僅接受 onCleanup,無法直接取得變更前後的值。 |
| 適合簡單副作用 | 如同步更新 DOM、觸發 API 請求、設定計時器等。 |
| 可設定執行時機 | flush 選項可控制在「同步」('sync')、'pre'(預渲染前)或 'post'(渲染後)執行。 |
範例 4:最簡單的 watchEffect
import { ref, watchEffect } from 'vue'
const message = ref('Hello')
watchEffect(() => {
console.log('訊息變更為:', message.value)
})
// 變更會自動觸發
message.value = 'World' // console: 訊息變更為: World
範例 5:使用 onCleanup 取消計時器
import { ref, watchEffect } from 'vue'
const seconds = ref(0)
watchEffect((onCleanup) => {
const timer = setInterval(() => {
seconds.value++
}, 1000)
// 每次副作用重新執行前,先清除上一次的計時器
onCleanup(() => clearInterval(timer))
})
範例 6:結合 flush: 'post' 與 UI 更新
import { ref, watchEffect } from 'vue'
const input = ref('')
// 先等 Vue 完成 DOM 更新後,再執行副作用(如聚焦)
watchEffect(
(onCleanup) => {
const el = document.getElementById('myInput')
if (el) el.focus()
},
{ flush: 'post' }
)
3. 何時選擇 watch、何時選擇 watchEffect
| 情境 | 建議使用 | 為什麼 |
|---|---|---|
| 需要比較新舊值 | watch |
可直接取得 newVal、oldVal。 |
| 監聽多個來源且需要分別處理 | watch |
可以傳入陣列或 getter,且回呼會一次收到所有值。 |
| 只想要在資料變化時執行副作用,且不在意來源 | watchEffect |
自動收集依賴,寫法更簡潔。 |
| 需要深度監聽物件 | watch(deep: true)或 watchEffect(自動深度追蹤) |
watchEffect 會追蹤所有使用到的屬性;watch 需要明確設定。 |
| 需要在副作用執行前先清理資源 | 兩者皆可,watch 使用 onCleanup 參數,watchEffect 直接接受 onCleanup。 |
|
| 想要在組件掛載時立即執行一次 | watchEffect(自動)或 watch(..., { immediate: true }) |
兩者皆可,但 watchEffect 更直觀。 |
| 需要控制執行時機(pre / post) | 兩者皆支援 flush 選項,但 watchEffect 在副作用函式內部更常使用。 |
常見陷阱與最佳實踐
1. 不小心造成無限迴圈
watchEffect(() => {
// 若在副作用裡直接改變被追蹤的值,會立即觸發再次執行
count.value++ // ❌ 無限遞增
})
解法:在副作用中避免直接改變同一個依賴,或使用 watch 並在回呼裡加入條件判斷。
2. deep: true 的效能成本
深度監聽會遍歷整個物件樹,對大型資料結構可能造成性能瓶頸。建議:
- 只在必要時使用
deep: true。 - 若只關注特定屬性,改用多個
watch或watchEffect只讀取需要的欄位。
3. watchEffect 失去新舊值資訊
有時候需要比較變更前後的值,卻誤用 watchEffect,導致無法取得舊值。解法:改用 watch,或自行在 watchEffect 內部儲存前一次的值。
let prev = null
watchEffect(() => {
const cur = data.value
if (prev !== null) {
console.log('變更前後:', prev, '→', cur)
}
prev = cur
})
4. 清理函式遺漏
在使用計時器、WebSocket、或第三方庫時,忘記在 onCleanup 中釋放資源會導致記憶體泄漏。最佳實踐:
watchEffect((onCleanup) => {
const socket = new WebSocket(url)
socket.addEventListener('message', handler)
onCleanup(() => {
socket.removeEventListener('message', handler)
socket.close()
})
})
5. 依賴收集錯誤(使用非響應式值)
watchEffect 只會追蹤 響應式 讀取。如果在副作用裡使用普通變數或 props 的解構,依賴不會被收集,導致不會重新執行。
// ❌ 錯誤範例
const { title } = props // 解構會失去響應性
watchEffect(() => {
console.log(title) // 改變 props.title 不會觸發
})
正確寫法:
watchEffect(() => {
console.log(props.title) // 直接讀取 props.title
})
實際應用場景
1. 表單驗證(使用 watch)
在複雜表單中,需要根據多個欄位的變化計算驗證結果,且必須比較前後值以決定是否顯示錯誤訊息。watch 的多來源與舊值參數最適合此情境。
import { reactive, watch } from 'vue'
const form = reactive({
email: '',
password: '',
confirm: ''
})
watch(
() => [form.email, form.password, form.confirm],
([email, password, confirm], [oldEmail]) => {
// 只在任一欄位變更時重新驗證
const errors = {}
if (!/.+@.+\..+/.test(email)) errors.email = 'Email 格式不正確'
if (password.length < 6) errors.password = '密碼長度不足'
if (password !== confirm) errors.confirm = '密碼不一致'
console.log('驗證結果', errors)
},
{ immediate: true }
)
2. 動態資料抓取(使用 watchEffect)
當某個搜尋關鍵字改變時,需要立即發送 API 請求並更新列表。watchEffect 可以自動追蹤關鍵字的變化,且結合 onCleanup 取消前一次的請求。
import { ref, watchEffect } from 'vue'
import axios from 'axios'
const keyword = ref('')
const results = ref([])
const controller = ref(null) // 用於取消前一次請求
watchEffect((onCleanup) => {
if (!keyword.value) {
results.value = []
return
}
// 取消上一次的請求
if (controller.value) controller.value.abort()
controller.value = new AbortController()
axios
.get('/api/search', {
params: { q: keyword.value },
signal: controller.value.signal
})
.then(res => (results.value = res.data))
.catch(err => {
if (err.name !== 'AbortError') console.error(err)
})
// 清理:在下一次關鍵字變更前取消請求
onCleanup(() => {
if (controller.value) controller.value.abort()
})
})
3. UI 同步動畫(使用 watchEffect + flush: 'post')
在 Vue 組件掛載後,需要根據計算屬性產生的尺寸來設定 CSS 動畫。使用 watchEffect 並設定 flush: 'post',保證在 DOM 完全渲染後才執行動畫。
import { ref, computed, watchEffect } from 'vue'
const items = ref([...]) // 陣列資料
const containerHeight = computed(() => items.value.length * 40 + 'px')
watchEffect(
(onCleanup) => {
const el = document.querySelector('.list')
el.style.height = containerHeight.value
// 觸發 CSS transition
requestAnimationFrame(() => {
el.classList.add('expand')
})
},
{ flush: 'post' }
)
總結
watch是 「明確監聽」 工具,適合需要 新舊值比較、深度監聽或多來源監聽 的情境。它提供immediate、deep、flush等選項,讓開發者可以精細控制執行時機與行為。watchEffect則是 「自動依賴收集」 的簡潔寫法,適合 單純副作用(如 UI 更新、API 請求、計時器)且不需要手動指定來源。它會在建立時立即執行,並在任何被使用的響應式值變化時重新執行。- 兩者都支援 清理函式 (
onCleanup) 以防止資源泄漏;但要注意 避免在副作用內直接改變同一個依賴,以免產生無限迴圈。 - 在實務開發中,先思考需求:是否需要比較前後值、是否要深度監聽、或只是單純的副作用?根據需求選擇最合適的 API,才能寫出 可讀、效能佳、易維護 的程式碼。
掌握 watch 與 watchEffect 的差異與最佳實踐,將大幅提升你在 Vue 3 中開發響應式應用的效率與品質。祝開發順利,期待你在專案中玩出更多創意!