前一篇說明 Vue 3 有 Proxy 管家的大絕「響應式」,但是除了響應式實戰上還會需要處理或是監控,所以第一階段的基礎理解與環境設定聊完後,今天開始我們一起進入第二階段的資料監控與處理。
首先,先來了解資料監控的 computed
與 watch
!
在 Vue 3 中,computed
和 watch
都能「監控資料變化」,但定位與使用場景完全不同:
computed
→ 資料衍生(算出新值),如果需要基於某些反應式資料計算出新的值,可以使用 computed
屬性。這些屬性會自動依賴相應的資料,並在其依賴改變時自動更新。watch
→ 副作用處理(觸發行為),當需要針對某些資料變化進行特定的操作時,可以使用 watch
。這讓你能夠在資料改變時執行副作用,例如傳送請求或更新其他狀態。初學時常搞混,今天我們一起徹底釐清兩者的差異與最佳實踐。
computed
(計算屬性) → 側重於「資料衍生」computed
是一個計算屬性,主要用於依賴其他資料計算得出新的屬性。當依賴的資料改變時,computed
會自動重新計算並更新其值。
computed
。ref
/ reactive
資料。其實,可以把它理解成 Excel 裡的公式儲存格,只要輸入變了,公式就會重新計算。
<script lang="ts" setup>
import { ref, computed } from 'vue'
// 基礎資料
const price = ref<number>(100)
const quantity = ref<number>(2)
// 衍生資料:總價
const total = computed<number>(() => price.value * quantity.value)
</script>
<template>
<div>總價:{{ total }}</div>
</template>
computed
常見坑computed
⚠️ 問題:computed
應該只回傳新值,不要觸發副作用。
const username = ref('')
const saveName = computed(() => {
localStorage.setItem('username', username.value) // 這是副作用
return username.value.toUpperCase()
})
✅ 解法:用 watch
做副作用,computed
專心算值。
⚠️ 問題:computed
只有在依賴的 ref
/ reactive
改變時才會重新計算,如果計算裡的變數不是 reactive
,computed
不會重新跑。
✅ 解法:確認你用的是 ref
/ reactive
,不是普通變數。
computed
當方法 (誤用場景)⚠️ 問題:有些人把 computed
當成 function 一直呼叫,結果沒必要的時候也重算。
✅ 解法:如果是「每次呼叫都要算一次」→ 用 methods
,不是 computed
,computed
適合「根據資料變化快取」,而不是「每次呼叫臨時計算」。
computed
setter 忘記寫(雙向綁定失敗)⚠️ 問題:如果用 v-model
綁定 computed
,但沒定義 setter,會報錯或沒反應。
✅ 解法:
<script lang="ts" setup>
import { ref, computed } from 'vue'
const firstName = ref<string>('Monica')
const lastName = ref<string>('ku')
// 雙向綁定 computed
const fullName = computed<string>({
get: () => `${firstName.value} ${lastName.value}`,
set: (val: string) => {
const parts = val.split(' ')
firstName.value = parts[0] || ''
lastName.value = parts[1] || ''
}
})
</script>
<template>
<input v-model="fullName" placeholder="輸入全名" />
<p>名字:{{ firstName }}</p>
<p>姓氏:{{ lastName }}</p>
</template>
watch
(監聽器) → 側重於「副作用處理」watch
是一個跟蹤資料變化的工具,當指定的資料變化時,會執行相應的回撥函式。適用於執行非同步操作或當需要執行某些操作時,而這些操作不需要計算新值。
可以理解成:監控資料變化,然後「觸發行為/副作用」。
computed
提到的坑嗎?把副作用寫進 computed
,因為 computed
只會回傳新值,所以不會觸發!類似的範例,如果要把資料寫入 localStorage 要改用 watch
<script lang="ts" setup>
import { ref, watch } from 'vue'
const username = ref<string>('')
// 資料變化 → 執行副作用
watch(username, (newVal: string) => {
localStorage.setItem('username', newVal)
})
</script>
<template>
<input v-model="username" placeholder="輸入名稱" />
</template>
watch
常見坑watch
觸發太頻繁⚠️ 問題:比如監聽 input
,每次輸入一個字就打 API。
✅ 解法:加 debounce
/ throttle
,或用 watchEffect
+ 計時器控制。
<script setup lang="ts">
import { ref, watch, onBeforeUnmount } from 'vue'
import axios, { AxiosResponse } from 'axios'
interface SearchResult { id: number; name: string }
const results = ref<SearchResult[] | null>(null)
const query = ref<string>('') // 使用泛型指定字串
const loading = ref<boolean>(false)
let timer: ReturnType<typeof setTimeout> | null = null
let controller: AbortController | null = null
/** 監聽輸入框 */
watch(query, (q: string) => {
if (timer) clearTimeout(timer) // 每次輸入重置計時器
if (!q) {
results.value = null
return
}
timer = setTimeout(async () => {
if (controller) controller.abort() // 取消上一個請求
controller = new AbortController()
loading.value = true
try {
const res: AxiosResponse<any> = await axios.get(
`/api/search`,
{
params: { q },
signal: controller.signal,
}
)
results.value = res.data
} catch (err: any) {
if (axios.isCancel(err)) {
console.log('Request canceled', err.message)
} else if (err.name === 'CanceledError') {
console.log('Fetch aborted')
} else {
console.error(err)
}
} finally {
loading.value = false
}
}, 500)
})
onBeforeUnmount(() => {
if (timer) clearTimeout(timer)
if (controller) controller.abort()
})
</script>
<template>
<input v-model="query" placeholder="搜尋..." />
<div v-if="loading">Loading...</div>
<pre>{{ results }}</pre>
</template>
deep: true
⚠️ 問題:reactive
物件或陣列裡的深層屬性改了,預設 watch
不會觸發。
✅ 解法:
<script lang="ts" setup>
import { reactive, watch } from 'vue'
interface User {
name: string
age: number
}
const user = reactive<User>({
name: 'kuku',
age: 20
})
// 監聽整個物件(需加 deep: true)
watch(
() => user,
(newVal, oldVal) => {
console.log('User 改變:', newVal, oldVal)
},
{ deep: true }
)
</script>
reactive
整個物件,拿不到新值⚠️ 問題:watch(reactiveObj, (newVal) => { ... })
其實拿到的 newVal
跟 oldVal
可能一樣,所以 newVal === oldVal
會是 true
,無法用物件參考比較來判斷「哪裡變了」。
<script setup lang="ts">
import { reactive, watch } from 'vue'
const state = reactive({
count: 0,
nested: { a: 1 }
})
// 當監聽整個 reactive 物件時,回呼的 newVal/oldVal 常為相同 Proxy
watch(state, (newVal, oldVal) => {
console.log('new === old ?', newVal === oldVal) // 通常會印 true
console.log('new.nested.a', newVal.nested.a, 'old.nested.a', oldVal.nested.a)
}, { deep: true })
// 觸發變更(在真實元件中通常是某個事件)
setTimeout(() => {
state.nested.a = 2
}, 1000)
</script>
✅ 解法:監聽具體屬性。
<script setup lang="ts">
import { reactive, watch } from 'vue'
const state = reactive({ user: { name: 'kuku', age: 20 } })
// 監聽巢狀屬性 user.name => 單一監聽
watch(
() => state.user.name,
(newName, oldName) => {
console.log('name changed:', oldName, '→', newName)
}
)
state.user.name = 'rober' // 會觸發
</script>
<script setup lang="ts">
import { reactive, watch } from 'vue'
const state = reactive({ a: 1, b: 2 })
// 當要同時監聽多個欄位並一次拿到所有新舊值
watch(
[() => state.a, () => state.b],
([newA, newB], [oldA, oldB]) => {
console.log('a', oldA, 'to =>', newA)
console.log('b', oldB, 'to =>', newB)
}
)
state.a = 10
state.b = 20
</script>
watch
⚠️ 問題:watch
必須在組件 setup 週期內呼叫,否則沒有有效的響應式上下文。
✅ 解法:把 watch
寫在 <script setup>
裡。
watch
當作 computed
⚠️ 問題:很多人用 watch
來「計算新資料」,但這樣不能快取,每次都重新跑。
<script setup lang="ts">
import { ref, watch } from 'vue'
const firstName = ref('Monica')
const lastName = ref('ku')
const fullName = ref('')
// 這樣用 watch,每次都會重算,而且 fullName 只是被動塞值
watch([firstName, lastName], ([f, l]) => {
fullName.value = `${f} ${l}`
})
</script>
<template>
<div>{{ fullName }}</div>
</template>
✅ 解法:如果是「資料衍生」→ computed
;「需要副作用」→ watch
。
<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref('Ada')
const lastName = ref('Lovelace')
// 直接「回傳一個響應式值」當變數用
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
</script>
<template>
<div>{{ fullName }}</div>
</template>