本文 AI 產出,尚未審核

Vue3 Composition API(核心)—— computed() 計算屬性

簡介

在 Vue3 中,Composition API 讓我們可以用函式的方式組織邏輯,提升程式碼的可讀性與可重用性。computed() 是 Composition API 中最常用的 API 之一,它負責建立衍生的、可快取的值,讓 UI 只在依賴的來源改變時重新計算。

如果你在開發大型表單、即時統計或多層級資料變換時,若仍使用 watch 逐一監聽,往往會產生冗餘的程式碼與不必要的效能開銷。computed() 正是解決這類需求的利器,讓 資料的衍生邏輯集中且自動快取,同時保持宣告式的寫法,提升開發效率與維護性。


核心概念

1. computed() 的基本使用

computed() 接收一個 getter 函式,回傳值會在依賴的響應式來源(refreactive)改變時重新計算。第一次取值時會執行 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)

有時候我們希望計算屬性不只是讀取值,還能「寫入」回原始資料。這時可以傳入一個包含 getset 的物件。

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 的重新計算;但因為 firstNamelastName 改變,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 應保持純粹,副作用會導致不可預期的重複執行。 使用 watchwatchEffect 處理 API、DOM 操作等。
忘記使用 .value 在 Composition API 中,ref 必須透過 .value 存取,否則會得到 Ref 物件本身。 始終ref.value;或使用 reactive 包裝物件避免 .value
過度嵌套 computed 雖然可以層層嵌套,但過深的依賴會降低可讀性。 盡量把衍生邏輯集中在同一個 computed,必要時抽成自訂函式。
使用非響應式資料作為依賴 若 getter 內部引用了普通變數,Vue 無法追蹤更新。 確保所有依賴都是 refreactivecomputed 本身。
忘記返回值 computed 的 getter 必須有 return,否則結果為 undefined 檢查 每個 getter 都有明確的回傳。

最佳實踐

  1. 保持 getter 純粹:只做計算,不修改狀態。
  2. 盡量使用 reactive 包裝物件:可以直接存取屬性,減少 .value 的噪音。
  3. 命名要語意化:如 isAdult, filteredList, totalPrice,讓使用者一眼看出意圖。
  4. 利用可寫 computed 處理雙向綁定(例如表單輸入),但仍保持副作用在 watch
  5. 測試 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 應用程式。祝開發順利! 🚀