watch 與 computed 的區別
watch 和 computed 都建立在 Vue 響應式系統上,也都會追蹤依賴。但它們解決的問題不同:computed 用來產生衍生值,watch 用來在資料變化時執行副作用。
一句話回答:如果你需要「根據現有狀態算出另一個值」,用 computed;如果你需要「資料變了之後做一件事」,例如請求 API、寫入 localStorage、操作 DOM,就用 watch。
一、 必考觀念
1. computed 是衍生狀態
computed 適合描述「一個值如何由其他狀態推導而來」。
import { computed, ref } from 'vue'
const price = ref(100)
const quantity = ref(2)
const total = computed(() => {
return price.value * quantity.value
})total 不應該手動維護,因為它永遠可以由 price 和 quantity 算出來。
這種場景如果用 watch,反而會多一份同步成本:
const total = ref(0)
watch([price, quantity], () => {
total.value = price.value * quantity.value
})這樣 total 變成另一份可變狀態。如果同步邏輯漏掉或時機錯了,就會出現資料不一致。
2. watch 是副作用
watch 適合在資料變化後做某件事。
import { ref, watch } from 'vue'
const keyword = ref('')
const results = ref([])
watch(keyword, async (value) => {
results.value = await searchUsers(value)
})這裡的 API 請求就是副作用。它不是單純「算出一個值」,而是觸發外部行為。
常見副作用:
- 請求 API。
- 同步 localStorage。
- 操作 DOM。
- 呼叫第三方套件。
- 根據 route params 載入資料。
- 上報 analytics。
3. computed 有快取,watch 沒有「回傳值快取」概念
computed 的 getter 會被快取,依賴不變就不重新算。
const expensiveValue = computed(() => {
return heavyCalculate(source.value)
})多次讀取:
expensiveValue.value
expensiveValue.value
expensiveValue.value只要 source.value 沒變,getter 不會重跑。
watch 不是用來產生可讀取的快取值。它關心的是「source 變了,callback 要不要執行」。
4. computed 通常應該是純函數
computed getter 應該只做同步計算並回傳值,不應該做副作用。
不建議:
const user = computed(async () => {
return await fetchUser(id.value)
})也不建議:
const total = computed(() => {
localStorage.setItem('price', String(price.value))
return price.value * quantity.value
})這些應該交給 watch。
二、 核心對比
| 面向 | computed | watch |
|---|---|---|
| 核心用途 | 產生衍生值 | 執行副作用 |
| 是否回傳值 | 是,回傳 computed ref | 否,回傳停止監聽函數 |
| 是否快取 | 有,依賴不變不重算 | callback 依 source 變化執行 |
| 是否 lazy | 是,讀取時才求值 | 預設 lazy,source 變化才 callback |
| 是否適合 async | 不適合 | 適合 |
| 是否應有副作用 | 不應該 | 可以 |
| 常見場景 | 篩選列表、總價、格式化文字 | API、DOM、localStorage、第三方套件 |
三、 原理差異
1. computed 的心智模型
computed 是一個特殊 ref:
讀 computed.value
-> 如果 dirty,執行 getter
-> getter 期間收集依賴
-> 快取結果
-> 依賴沒變,下次直接回傳快取依賴變化時:
dependency trigger
-> computed 標記 dirty
-> 通知依賴 computed 的 effect
-> 下次讀取 computed.value 時重新計算如果你想看更細的實現,可以讀:Vue computed 的實現原理。
2. watch 的心智模型
watch 是建立一個監聽 effect。它會追蹤 source 的變化,當新舊值不同時執行 callback。
watch(source, (newValue, oldValue) => {
// source 變化後執行
})簡化流程:
第一次收集 source 依賴
-> source 變化
-> scheduler 安排 callback
-> 重新取得 newValue
-> 比較 newValue / oldValue
-> 執行 callback3. watch 預設也是 lazy
很多人以為 watch 會立刻執行。其實 watch 預設不會立即執行 callback,只有 source 變化後才執行。
watch(keyword, () => {
console.log('changed')
})如果你希望建立 watch 時立刻執行一次:
watch(
keyword,
() => {
console.log('run now and when changed')
},
{ immediate: true },
)四、 watch 的常見選項
1. immediate
immediate 讓 callback 在建立 watch 時先執行一次。
watch(
userId,
async (id) => {
user.value = await fetchUser(id)
},
{ immediate: true },
)適合「進頁面先載入一次,之後 id 變了再載入」。
2. deep
deep 用來深層監聽物件內部變化。
watch(
form,
() => {
saveDraft(form)
},
{ deep: true },
)要小心:deep watch 需要遍歷深層資料,資料很大時可能有成本。很多時候更好的做法是 watch 精準欄位。
watch(
() => form.email,
(email) => {
validateEmail(email)
},
)3. flush
flush 控制 callback 執行時機。
| flush | 時機 |
|---|---|
pre | 預設,元件 DOM 更新前 |
post | 元件 DOM 更新後 |
sync | 同步執行 |
如果 callback 需要讀更新後的 DOM,可以用:
watch(
visible,
() => {
inputRef.value?.focus()
},
{ flush: 'post' },
)這和 nextTick 很接近。延伸讀:nextTick 的作用與實現原理。
4. cleanup
watch callback 可以註冊清理函數,避免競態或資源泄漏。
watch(keyword, async (value, _oldValue, onCleanup) => {
const controller = new AbortController()
onCleanup(() => {
controller.abort()
})
results.value = await fetch(`/api/search?q=${value}`, {
signal: controller.signal,
}).then(res => res.json())
})當 keyword 很快變化時,上一個請求可以被取消。
五、 watchEffect 是什麼?
watchEffect 會自動追蹤 callback 裡讀到的響應式依賴。
watchEffect(() => {
console.log(userId.value)
console.log(locale.value)
})它不需要你明確指定 source。
對比:
watch([userId, locale], () => {
// 明確監聽 userId 和 locale
})| API | 特點 |
|---|---|
watch | 明確 source,能拿 new/old value |
watchEffect | 自動收集依賴,立即執行 |
watchEffect 適合依賴簡單、探索性或需要自動追蹤的副作用;watch 適合明確控制資料來源、比較新舊值、處理精準副作用。
六、 追問題庫
Q1:能用 watch 實現 computed 嗎?
可以,但不建議。
const fullName = ref('')
watch([firstName, lastName], () => {
fullName.value = `${firstName.value} ${lastName.value}`
}, { immediate: true })這會讓衍生值變成手動同步狀態。computed 更直覺,也更不容易出錯。
Q2:能用 computed 取代 watch 嗎?
如果只是算值,可以。
如果要做副作用,不應該。
// 適合 computed
const isValid = computed(() => email.value.includes('@'))
// 適合 watch
watch(isValid, (value) => {
localStorage.setItem('valid', String(value))
})Q3:computed 裡可以寫 async 嗎?
不建議。
computed 期望回傳同步衍生值。如果 getter 是 async,回傳的是 Promise,不是最終資料,而且快取的是 Promise 本身,容易讓狀態語意混亂。
Q4:watch 監聽物件時 newValue 和 oldValue 為什麼可能一樣?
如果是 deep watch 同一個 reactive object,內部屬性變化時,新舊值可能指向同一個物件引用。
watch(
form,
(newValue, oldValue) => {
console.log(newValue === oldValue) // 可能是 true
},
{ deep: true },
)如果你需要比較變化,可能要 watch 具體欄位,或自己建立快照。
Q5:watch 會不會造成無限循環?
會,如果你在 watch 裡修改它正在監聽的 source,而且沒有終止條件。
watch(count, () => {
count.value++
})這會一直觸發自己。實務上要避免這種設計,或加明確條件。
七、 實作題
題目:搜尋頁該用 computed 還是 watch?
需求:
- 有一份本地 users。
- 使用 keyword 篩選 users。
- keyword 改變後,同步 query string。
- keyword 改變後,向後端搜尋遠端資料。
本地篩選適合 computed:
const filteredUsers = computed(() => {
return users.value.filter(user => {
return user.name.includes(keyword.value)
})
})同步 query string 適合 watch:
watch(keyword, (value) => {
router.replace({
query: {
...route.query,
keyword: value || undefined,
},
})
})遠端搜尋也適合 watch:
watch(keyword, async (value, _oldValue, onCleanup) => {
const controller = new AbortController()
onCleanup(() => {
controller.abort()
})
remoteUsers.value = await fetchUsers(value, controller.signal)
})同一個 keyword,可以同時搭配 computed 和 watch。重點是分清楚:
- computed 負責同步推導值。
- watch 負責外部副作用。
八、 資深視角
1. 先問「這是值,還是事情?」
選 API 的第一個問題:
我現在要得到一個值?
還是資料變化後要做一件事?如果是值,用 computed。
如果是事情,用 watch。
這個判斷比背 API 選項更重要。
2. watch 很強,但容易藏資料流
watch 可以做很多事,也因此容易讓資料流變隱性。
例如:
watch(a, () => {
b.value = transform(a.value)
})如果 b 其實只是 a 的衍生值,這段就不如 computed 清楚。
watch 應該用在真的需要副作用的地方。否則系統裡到處都是 watch,會很難追蹤資料如何變化。
3. 副作用要注意清理
watch 常用來接 API、timer、第三方套件。這些都可能需要清理。
好的 watch 不只是能執行 callback,還要考慮:
- 快速變化時是否有競態?
- 舊請求是否要取消?
- 元件卸載時是否清理?
- 是否需要 debounce?
- callback 時機應該是
pre、post還是sync?
總結
| 問題 | 面試回答重點 |
|---|---|
| computed 用途 | 產生同步衍生值 |
| watch 用途 | 監聽變化並執行副作用 |
| 快取 | computed 有快取,watch 不以快取值為目的 |
| lazy | computed 讀取才求值,watch 預設 source 變化才 callback |
| async | computed 不適合,watch 適合 |
| DOM 時機 | watch 可用 flush: 'post' |
| 選擇原則 | 要值用 computed,要做事用 watch |
一句完整的面試回答可以是:
computed和watch都依賴 Vue 的響應式追蹤,但用途不同。computed用來描述衍生狀態,它有快取、惰性求值,依賴不變時多次讀取不會重新計算;它應該保持同步且無副作用。watch用來監聽資料變化並執行副作用,例如 API 請求、同步 localStorage、操作 DOM 或更新路由 query。實務上,如果我要得到一個由狀態推導出的值,用 computed;如果我要在狀態變化後做一件事,用 watch。