Appearance
computed 與 watch
前面幾節我們已經能顯示資料、處理事件、渲染列表。接著要看兩個很常用、也很容易混淆的工具:computed 與 watch。
簡單來說,computed 用來產生「由既有資料推導出的結果」;watch 用來在資料變動時執行副作用,例如呼叫 API、寫入 localStorage 或記錄分析事件。
用 computed 表示衍生資料
假設我們有一份待辦清單,畫面上要顯示未完成項目數量。這個數字不應該另外手動維護,因為它可以從 todos 推導出來:
vue
<script setup>
import { computed, ref } from 'vue'
const todos = ref([
{ id: 1, text: '建立 Vite 專案', done: true },
{ id: 2, text: '認識響應式資料', done: true },
{ id: 3, text: '練習 computed', done: false }
])
const remainingCount = computed(() =>
todos.value.filter(todo => !todo.done).length
)
</script>
<template>
<main class="app">
<p>還有 {{ remainingCount }} 件事尚未完成。</p>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
</li>
</ul>
</main>
</template>
<style scoped>
.app {
padding: 2rem;
}
</style>remainingCount 會依賴 todos。只要 todos 改變,Vue 就知道這個 computed 結果需要重新計算。
computed vs methods
同樣的結果,也可以寫成函式:
js
function getRemainingCount() {
return todos.value.filter(todo => !todo.done).length
}差異在於:computed 會根據依賴快取結果。只要 todos 沒變,重複讀取 remainingCount 不會重新計算;methods 則是每次呼叫都會執行一次。
實務上可以這樣判斷:
| 情境 | 建議 |
|---|---|
| 要把既有資料推導成另一個值,並在模板中重複使用 | 用 computed |
| 使用者操作時要執行一段動作 | 用 function |
| 要因資料變化而做副作用 | 用 watch |
computed getter 要保持純粹
computed 的 getter 應該只負責計算並回傳結果,不要在裡面修改其他狀態、呼叫 API 或操作 DOM。
常見錯誤:在 computed 裡做副作用或忘記 return
下面這兩種寫法都容易出問題:
js
const total = computed(() => {
console.log('重新計算') // 不建議:computed 應避免副作用
todos.value.length // 錯誤:沒有 return,結果會是 undefined
})沒有 return 的問題很直接:getter 回傳 undefined,total 的值就永遠是 undefined。
副作用的問題則出在執行時機:computed 有快取,而且採惰性計算。依賴改變時,Vue 只會先把快取標記為需要更新;要等到下一次有人讀取這個 computed 的值,getter 才會重新執行。所以 getter 並不是「資料一變就會觸發的 callback」。
這代表放在 getter 裡的 console.log、API 呼叫等動作,會變成「有時發生、有時不發生」,出問題時很難追查;如果是在 getter 裡修改其他響應式資料,還可能牽動其他 computed 跟著重算,讓資料流變得難以預測。
比較好的寫法是讓 computed 專心回傳結果:
js
const total = computed(() => todos.value.length)如果真的需要在資料變動時執行副作用,請改用 watch。
用 watch 觀察資料變化
watch 適合用在「資料變了,所以要做某件事」的場景。下面範例會在關鍵字改變時更新提示文字:
vue
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
const message = ref('請輸入關鍵字')
watch(keyword, (newKeyword, oldKeyword) => {
if (newKeyword === '') {
message.value = '請輸入關鍵字'
return
}
message.value = `從「${oldKeyword}」改成「${newKeyword}」`
})
</script>
<template>
<main class="app">
<input v-model="keyword" type="search" placeholder="搜尋">
<p>{{ message }}</p>
</main>
</template>
<style scoped>
.app {
padding: 2rem;
}
</style>watch(source, callback) 的第一個參數是要觀察的資料來源,第二個參數會在資料變動時執行。callback 可以取得新值與舊值。
什麼時候不要用 watch?
如果只是要從既有資料算出另一個結果,通常不需要 watch。例如下面這種寫法,就是用 watch 手動維護衍生資料:
js
const firstName = ref('Ada')
const lastName = ref('Lovelace')
const fullName = ref('')
watch([firstName, lastName], ([newFirstName, newLastName]) => {
fullName.value = `${newFirstName} ${newLastName}`
})這種情境改成 computed 會更清楚:
js
const fullName = computed(() => `${firstName.value} ${lastName.value}`)常見錯誤:該用 computed 卻用 watch
watch 很強大,但不要把它當成「資料加工工具」。只要結果可以由現有資料同步推導出來,先考慮 computed。
把 watch 留給副作用,程式會比較容易追蹤。
立即執行與深層觀察
watch 預設會等資料第一次變動後才執行。如果建立 watcher 時就想先跑一次,可以加上 immediate: true:
js
watch(keyword, (newKeyword) => {
message.value = newKeyword === ''
? '請輸入關鍵字'
: `搜尋:${newKeyword}`
}, { immediate: true })如果觀察的是物件或陣列中的深層資料,則可能會需要 deep。深層觀察會遍歷巢狀屬性,因此資料很大時要謹慎使用。
watch deep 數字深度 (Vue 3.5 新增)
在 Vue 3.5 之後,deep 不只可以是 true,也可以是數字,用來限制最多往下觀察幾層。以下面範例的 profile 為例,{ deep: 2 } 的觀察範圍只到第二層:
vue
<script setup>
import { ref, watch } from 'vue'
const profile = ref({
name: 'Ada',
settings: {
theme: {
color: 'blue'
}
}
})
const triggeredCount = ref(0)
watch(
profile,
() => {
triggeredCount.value++
},
{ deep: 2 }
)
function replaceTheme() {
// theme 是第二層的屬性,仍在 deep: 2 的觀察範圍內
profile.value.settings.theme = { color: 'green' }
}
function changeColor() {
// color 位在第三層,超出 deep: 2 的範圍
profile.value.settings.theme.color = 'red'
}
</script>
<template>
<main class="app">
<p>目前顏色:{{ profile.settings.theme.color }}</p>
<p>watch 觸發次數:{{ triggeredCount }}</p>
<button type="button" @click="replaceTheme">
換掉整組 theme
</button>
<button type="button" @click="changeColor">
只改最深層的 color
</button>
</main>
</template>
<style scoped>
.app {
padding: 2rem;
}
</style>{ deep: 2 } 的意思是最多往下遍歷兩層。實際按按鈕觀察:按「換掉整組 theme」時,改動發生在第二層,watch 觸發次數會加一;按「只改最深層的 color」時,改動發生在第三層,watch 不會觸發,但畫面上的顏色照樣更新。
從這裡也可以看出,deep 只限制 watch 的觀察範圍,不影響響應式系統本身。數字深度比無限制的深層觀察更可控,但仍然要避免拿來觀察過大的資料結構。
小提醒:在 watch 裡發非同步請求要小心
當 watch 裡面發出非同步請求時,還會遇到「舊請求比新請求晚回來」的問題。Vue 3.5 的 onWatcherCleanup() 可以處理這類清理需求;這屬於 composables 與副作用管理的主題,CH5 會有更完整的說明。
