Vue3 Composition API(核心)—— computed() 計算屬性
簡介
在 Vue3 中,Composition API 讓我們可以用函式的方式組織邏輯,提升程式碼的可讀性與可重用性。computed() 是 Composition API 中最常用的 API 之一,它負責建立衍生的、可快取的值,讓 UI 只在依賴的來源改變時重新計算。
如果你在開發大型表單、即時統計或多層級資料變換時,若仍使用 watch 逐一監聽,往往會產生冗餘的程式碼與不必要的效能開銷。computed() 正是解決這類需求的利器,讓 資料的衍生邏輯集中且自動快取,同時保持宣告式的寫法,提升開發效率與維護性。
核心概念
1. computed() 的基本使用
computed() 接收一個 getter 函式,回傳值會在依賴的響應式來源(ref、reactive)改變時重新計算。第一次取值時會執行 getter,之後只要依賴未變,直接回傳上一次的快取結果。
import { ref, computed } from 'vue'
const price = ref(1200) // 原始資料
const taxRate = ref(0.1) // 稅率
// 計算含稅價格
const total = computed(() => {
return price.value * (1 + taxRate.value)
})
console.log(total.value) // 1320
price.value = 1500
console.log(total.value) // 1650(自動重新計算)
重點:
computed只在 依賴 改變時才會重新執行 getter,這就是「快取」的概念。
2. 可寫的 computed(getter / setter)
有時候我們希望計算屬性不只是讀取值,還能「寫入」回原始資料。這時可以傳入一個包含 get 與 set 的物件。
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// 透過 getter / setter 組合全名
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newName) {
const parts = newName.split(' ')
firstName.value = parts[0] || ''
lastName.value = parts[1] || ''
}
})
console.log(fullName.value) // John Doe
fullName.value = 'Jane Smith'
console.log(firstName.value) // Jane
console.log(lastName.value) // Smith
技巧:當
set被呼叫時,不會觸發get的重新計算;但因為firstName、lastName改變,fullName的快取會自動失效,下次取值時會重新執行get。
3. 多層依賴與嵌套 computed
computed 可以相互依賴,形成層級結構。Vue 會自動追蹤依賴關係,確保最底層變化時,所有受影響的計算屬性都會正確更新。
import { ref, computed } from 'vue'
const items = ref([
{ price: 100, qty: 2 },
{ price: 250, qty: 1 },
])
// 單項小計
const subtotals = computed(() => {
return items.value.map(i => i.price * i.qty)
})
// 總金額
const grandTotal = computed(() => {
return subtotals.value.reduce((sum, v) => sum + v, 0)
})
console.log(grandTotal.value) // 450
items.value[0].qty = 3
console.log(grandTotal.value) // 550(自動重新計算)
注意:即使
subtotals本身是一個陣列,grandTotal仍會正確追蹤subtotals的變化,因為 Vue 會在 getter 執行時「收集」所有依賴。
4. 與 watch 的差異
watch 用於副作用(side‑effects),例如發送 API、更新外部狀態。computed 則是純粹的衍生資料,不應包含副作用。
// ✅ 正確:使用 computed 產生衍生資料
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// ❌ 錯誤:在 computed 內部執行 API 呼叫
const userInfo = computed(() => {
fetch('/api/user') // ← 副作用,應改用 watch 或 async setup
})
5. 取得 computed 的原始值與快取控制
在某些情況下,我們需要直接取得 未快取 的值(例如測試或強制重新計算)。computed 本身沒有提供直接的方法,但可以透過 依賴的 ref 強制觸發。
import { ref, computed, triggerRef } from 'vue'
const counter = ref(0)
const double = computed(() => counter.value * 2)
console.log(double.value) // 0
counter.value = 5
console.log(double.value) // 10
// 強制讓 double 失效(不改變 counter)
triggerRef(counter) // 會使所有依賴 counter 的 computed 重新計算
console.log(double.value) // 10(重新計算,但值不變)
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 將副作用寫在 getter | computed 應保持純粹,副作用會導致不可預期的重複執行。 |
使用 watch 或 watchEffect 處理 API、DOM 操作等。 |
忘記使用 .value |
在 Composition API 中,ref 必須透過 .value 存取,否則會得到 Ref 物件本身。 |
始終 用 ref.value;或使用 reactive 包裝物件避免 .value。 |
| 過度嵌套 computed | 雖然可以層層嵌套,但過深的依賴會降低可讀性。 | 盡量把衍生邏輯集中在同一個 computed,必要時抽成自訂函式。 |
| 使用非響應式資料作為依賴 | 若 getter 內部引用了普通變數,Vue 無法追蹤更新。 | 確保所有依賴都是 ref、reactive 或 computed 本身。 |
| 忘記返回值 | computed 的 getter 必須有 return,否則結果為 undefined。 |
檢查 每個 getter 都有明確的回傳。 |
最佳實踐
- 保持 getter 純粹:只做計算,不修改狀態。
- 盡量使用
reactive包裝物件:可以直接存取屬性,減少.value的噪音。 - 命名要語意化:如
isAdult,filteredList,totalPrice,讓使用者一眼看出意圖。 - 利用可寫 computed 處理雙向綁定(例如表單輸入),但仍保持副作用在
watch。 - 測試 computed:在單元測試中,直接檢查
computed.value,確保邏輯正確。
實際應用場景
1. 表單驗證
在註冊表單中,需要即時顯示「密碼強度」與「兩次密碼是否相符」的訊息。使用 computed 可以把驗證規則抽離為衍生屬性,讓 UI 自動更新。
import { ref, computed } from 'vue'
const password = ref('')
const confirmPwd = ref('')
const passwordStrength = computed(() => {
if (password.value.length > 10) return '強'
if (password.value.length > 5) return '中'
return '弱'
})
const passwordsMatch = computed(() => password.value === confirmPwd.value)
2. 列表過濾與分頁
當使用者在大量資料表格中搜尋或切換分頁,computed 能快速產生過濾後的子集合,而不需要每次點擊都手動遍歷。
import { ref, computed } from 'vue'
const rawList = ref([...]) // 原始資料陣列
const keyword = ref('')
const currentPage = ref(1)
const pageSize = 10
const filtered = computed(() => {
return rawList.value.filter(item => item.name.includes(keyword.value))
})
const paged = computed(() => {
const start = (currentPage.value - 1) * pageSize
return filtered.value.slice(start, start + pageSize)
})
3. 依賴多個 API 回傳結果的彙總
假設有兩個 API 分別回傳「訂單」與「付款」資訊,需要計算「未付款金額」。computed 能自動根據兩個 ref 的變化重新計算。
import { ref, computed } from 'vue'
const orders = ref([]) // [{ id, amount }]
const payments = ref([]) // [{ orderId, paid }]
const unpaidTotal = computed(() => {
const paidMap = new Map(payments.value.map(p => [p.orderId, p.paid]))
return orders.value.reduce((sum, o) => {
const paid = paidMap.get(o.id) || 0
return sum + Math.max(0, o.amount - paid)
}, 0)
})
總結
computed() 是 Vue3 Composition API 中 不可或缺 的工具,提供快取且響應式的衍生資料。透過純粹的 getter(或 getter/setter)我們可以把複雜的計算邏輯抽離出來,讓組件的 template 更加簡潔,且在資料變動時自動更新。
在實務開發中,避免在 computed 中加入副作用,使用 watch 處理非同步或 DOM 操作;同時,善用可寫 computed 來完成雙向綁定,並以 reactive 包裝物件降低 .value 的使用頻率。只要遵守上述最佳實踐,computed() 能幫助你寫出 易讀、效能佳且易於維護 的 Vue3 應用程式。祝開發順利! 🚀