Vue Computed 計算屬性
Computed 計算屬性(Computed Properties)是用來根據其他響應式資料計算出衍生值的方式。它會自動追蹤依賴,並且只有在依賴改變時才會重新計算,具有快取機制。
基本用法
使用 computed() 函式建立計算屬性:
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// 計算屬性
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
</script>
<template>
<p>{{ fullName }}</p>
</template>
為什麼需要 Computed?
比較:在模板中直接寫表達式
<template>
<!-- 不推薦:模板中放太多邏輯 -->
<p>{{ firstName + ' ' + lastName }}</p>
<p>{{ items.filter(item => item.active).length }}</p>
</template>
雖然可以在模板中直接寫表達式,但這樣有幾個問題:
- 模板變得複雜難讀
- 如果多處使用同一個邏輯,會重複寫
- 沒有快取,每次渲染都會重新執行
比較:使用方法(Methods)
<script setup>
import { ref } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// 方法
function getFullName() {
return `${firstName.value} ${lastName.value}`
}
</script>
<template>
<!-- 每次渲染都會呼叫 -->
<p>{{ getFullName() }}</p>
</template>
方法每次渲染都會執行,沒有快取機制。
使用 Computed
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// 計算屬性 - 有快取
const fullName = computed(() => {
console.log('計算 fullName') // 只有依賴改變時才會執行
return `${firstName.value} ${lastName.value}`
})
</script>
<template>
<!-- 多次使用,只計算一次 -->
<p>{{ fullName }}</p>
<p>{{ fullName }}</p>
<p>{{ fullName }}</p>
</template>
Computed 的快取機制
Computed 會根據其響應式依賴進行快取。只有當依賴改變時,才會重新計算:
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const unrelated = ref('hello')
const doubled = computed(() => {
console.log('計算 doubled')
return count.value * 2
})
function incrementCount() {
count.value++ // 會觸發 doubled 重新計算
}
function changeUnrelated() {
unrelated.value = 'world' // 不會觸發 doubled 重新計算
}
</script>
<template>
<p>Count: {{ count }}</p>
<p>Doubled: {{ doubled }}</p>
<p>Unrelated: {{ unrelated }}</p>
<button @click="incrementCount">+1</button>
<button @click="changeUnrelated">改變 unrelated</button>
</template>
可寫的 Computed
預設情況下,computed 是唯讀的。如果需要可寫,可以提供 getter 和 setter:
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
// getter
get() {
return `${firstName.value} ${lastName.value}`
},
// setter
set(newValue) {
const names = newValue.split(' ')
firstName.value = names[0]
lastName.value = names[names.length - 1]
}
})
</script>
<template>
<input v-model="fullName">
<p>First Name: {{ firstName }}</p>
<p>Last Name: {{ lastName }}</p>
</template>
當你修改 fullName.value 時,setter 會被呼叫,並更新 firstName 和 lastName。
雖然可以建立可寫的 computed,但應該謹慎使用。Computed 主要用途是衍生資料,過多的副作用會讓程式碼難以理解。
常見使用情境
過濾與排序列表
<script setup>
import { ref, computed } from 'vue'
const items = ref([
{ id: 1, name: 'Apple', price: 30, inStock: true },
{ id: 2, name: 'Banana', price: 20, inStock: false },
{ id: 3, name: 'Orange', price: 25, inStock: true }
])
const searchQuery = ref('')
const showInStockOnly = ref(false)
const sortBy = ref('name')
const filteredItems = computed(() => {
let result = items.value
// 過濾:搜尋
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(item =>
item.name.toLowerCase().includes(query)
)
}
// 過濾:只顯示有庫存
if (showInStockOnly.value) {
result = result.filter(item => item.inStock)
}
// 排序
return [...result].sort((a, b) => {
if (sortBy.value === 'name') {
return a.name.localeCompare(b.name)
} else if (sortBy.value === 'price') {
return a.price - b.price
}
return 0
})
})
</script>
<template>
<input v-model="searchQuery" placeholder="搜尋...">
<label>
<input type="checkbox" v-model="showInStockOnly">
只顯示有庫存
</label>
<select v-model="sortBy">
<option value="name">依名稱排序</option>
<option value="price">依價格排序</option>
</select>
<ul>
<li v-for="item in filteredItems" :key="item.id">
{{ item.name }} - ${{ item.price }}
<span v-if="!item.inStock">(無庫存)</span>
</li>
</ul>
</template>
格式化顯示
<script setup>
import { ref, computed } from 'vue'
const price = ref(1234.5)
const date = ref(new Date())
// 格式化價格
const formattedPrice = computed(() => {
return new Intl.NumberFormat('zh-TW', {
style: 'currency',
currency: 'TWD'
}).format(price.value)
})
// 格式化日期
const formattedDate = computed(() => {
return new Intl.DateTimeFormat('zh-TW', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
}).format(date.value)
})
</script>
<template>
<p>價格:{{ formattedPrice }}</p>
<p>日期:{{ formattedDate }}</p>
</template>
統計數據
<script setup>
import { ref, computed } from 'vue'
const todos = ref([
{ id: 1, text: '學習 Vue', done: true },
{ id: 2, text: '寫專案', done: false },
{ id: 3, text: '部署上線', done: false }
])
const totalCount = computed(() => todos.value.length)
const doneCount = computed(() => todos.value.filter(t => t.done).length)
const pendingCount = computed(() => totalCount.value - doneCount.value)
const progress = computed(() => {
if (totalCount.value === 0) return 0
return Math.round((doneCount.value / totalCount.value) * 100)
})
</script>
<template>
<p>總計:{{ totalCount }} 項</p>
<p>已完成:{{ doneCount }} 項</p>
<p>待完成:{{ pendingCount }} 項</p>
<p>進度:{{ progress }}%</p>
<progress :value="doneCount" :max="totalCount"></progress>
</template>
Class 和 Style 綁定
<script setup>
import { ref, computed } from 'vue'
const isActive = ref(true)
const hasError = ref(false)
const activeColor = ref('blue')
const fontSize = ref(16)
const classObject = computed(() => ({
active: isActive.value,
'text-danger': hasError.value
}))
const styleObject = computed(() => ({
color: activeColor.value,
fontSize: fontSize.value + 'px'
}))
</script>
<template>
<div :class="classObject" :style="styleObject">
Hello Vue!
</div>
</template>
Computed 最佳實踐
1. 保持 Getter 無副作用
Computed getter 應該只做計算,不應該有副作用:
// ❌ 不好:有副作用
const fullName = computed(() => {
// 不要在 getter 中修改其他響應式資料
otherState.value = 'changed'
// 不要做 API 呼叫
fetch('/api/log')
return `${firstName.value} ${lastName.value}`
})
// ✅ 好:純粹的計算
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
2. 避免修改 Computed 的值
const doubled = computed(() => count.value * 2)
// ❌ 不好:直接修改 computed 的值
doubled.value = 10 // 會報錯
// ✅ 好:修改依賴的資料
count.value = 5
3. 適時使用 Computed 而非 Watch
如果你只是要根據資料變化計算出新值,使用 computed 比 watch 更簡潔:
// ❌ 不必要的 watch
const fullName = ref('')
watch([firstName, lastName], () => {
fullName.value = `${firstName.value} ${lastName.value}`
}, { immediate: true })
// ✅ 使用 computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
4. 考慮效能
複雜的計算應該避免放在會頻繁觸發的地方:
// 如果 items 很大,這個計算可能很耗時
const expensiveComputed = computed(() => {
return items.value
.filter(/* 複雜條件 */)
.map(/* 複雜轉換 */)
.reduce(/* 複雜計算 */)
})
// 可以考慮使用 watch + debounce,或分段處理
與 TypeScript 一起使用
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Todo {
id: number
text: string
done: boolean
}
const todos = ref<Todo[]>([])
// 型別會自動推斷
const doneCount = computed(() => {
return todos.value.filter(t => t.done).length
}) // computed 的型別是 ComputedRef<number>
// 也可以明確指定
const pendingTodos = computed<Todo[]>(() => {
return todos.value.filter(t => !t.done)
})
</script>