本文 AI 產出,尚未審核

Vue3 – Options API(傳統寫法)

主題:computed


簡介

在 Vue3 中,computed 是最常用的衍生(derived)資料來源之一。它可以根據其他反應式(reactive)狀態自動計算出新值,且具備 快取(caching) 機制,只有當相依的來源發生變化時才會重新求值。對於需要在模板中顯示衍生資料、或在多個地方共用相同計算結果的情境,computed 是不可或缺的工具。

即使在 Vue3 推出 Composition API,Options API 仍是許多既有專案與新手入門的首選寫法。掌握 computed 的使用方式,能讓你在不改變既有結構的前提下,寫出更簡潔、效能更佳的程式碼。


核心概念

1. computed 的基本語法

在 Options API 中,computed 必須在 export default {}computed 屬性裡宣告,回傳一個函式或一個具有 get / set 的物件。

export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    // 只讀 computed
    fullName() {
      return `${this.firstName} ${this.lastName}`
    }
  }
}
  • 快取fullName 只會在 firstNamelastName 改變時重新計算,其他 re‑render 不會觸發函式執行。
  • 依賴追蹤:Vue 會自動追蹤 this.firstNamethis.lastName 兩個依賴。

2. 具備 Setter 的 Computed(雙向綁定)

有時候需要讓 computed 既能讀也能寫,這時可以提供 set 方法。

export default {
  data() {
    return {
      firstName: '',
      lastName: ''
    }
  },
  computed: {
    fullName: {
      get() {
        return `${this.firstName} ${this.lastName}`.trim()
      },
      set(value) {
        const parts = value.split(' ')
        this.firstName = parts[0] || ''
        this.lastName = parts.slice(1).join(' ') || ''
      }
    }
  }
}

使用情境:在表單中直接綁定 v-model="fullName",使用者編輯完整姓名時,背後的 firstNamelastName 也會同步更新。

3. 多層依賴的 Computed

computed 可以相互依賴,形成「計算鏈」。只要最底層的依賴變動,整條鏈都會重新計算。

export default {
  data() {
    return {
      numbers: [1, 2, 3, 4, 5]
    }
  },
  computed: {
    // 先過濾出偶數
    evenNumbers() {
      return this.numbers.filter(n => n % 2 === 0)
    },
    // 再計算偶數總和
    sumEven() {
      return this.evenNumbers.reduce((acc, n) => acc + n, 0)
    }
  }
}
  • numbers 被推入新元素時,evenNumbers 重新計算,接著 sumEven 也會自動更新。

4. Computed 與方法(method)的差別

computed method
快取 有(除非依賴變更) 無,每次呼叫都會執行
適用情境 需要重複使用相同結果的衍生資料 僅在一次性操作或副作用較重的情況
語意 「這是一個值」 「這是一個行為」

範例:若只在模板中顯示過濾後的清單,使用 computed 能避免每次渲染都重新過濾。

5. 使用 computed 產生「懶」的非同步資料(進階技巧)

computed 本身是同步的,但可以結合 Promiseasync 函式,搭配 watchEffectwatch 產生「懶」的非同步結果。

export default {
  data() {
    return {
      userId: 1,
      userInfo: null
    }
  },
  computed: {
    // 產生一個 Promise,只有在 userId 改變時才會重新執行
    userPromise() {
      return fetch(`https://jsonplaceholder.typicode.com/users/${this.userId}`)
        .then(res => res.json())
    }
  },
  watch: {
    // 監聽 computed 回傳的 Promise,解析後寫入 data
    userPromise: {
      immediate: true,
      handler(promise) {
        promise.then(data => {
          this.userInfo = data
        })
      }
    }
  }
}
  • 雖然不建議把大量非同步邏輯放在 computed,但在「依賴變動才需要重新抓資料」的情況下,此技巧可以減少不必要的 API 呼叫。

程式碼範例

以下提供 5 個實用範例,每個範例皆包含完整說明與常見應用。

範例 1:簡易的字串拼接(只讀)

export default {
  data() {
    return {
      firstName: 'Jane',
      lastName: 'Smith'
    }
  },
  computed: {
    // **只讀** computed,用於顯示完整姓名
    fullName() {
      return `${this.firstName} ${this.lastName}`
    }
  },
  // template
  // <p>姓名:{{ fullName }}</p>
}

說明fullName 只會在 firstNamelastName 改變時重新計算,適合在多處顯示同一資訊。


範例 2:雙向綁定的完整地址

export default {
  data() {
    return {
      street: '',
      city: '',
      zip: ''
    }
  },
  computed: {
    address: {
      // 讀取完整地址
      get() {
        return `${this.street}, ${this.city} ${this.zip}`.replace(/^, | ,/g, '')
      },
      // 設定時自動拆解回各欄位
      set(value) {
        const [street, cityZip] = value.split(',')
        this.street = street?.trim() || ''
        const [city, zip] = cityZip?.trim().split(' ') || []
        this.city = city?.trim() || ''
        this.zip = zip?.trim() || ''
      }
    }
  }
  // template
  // <input v-model="address" placeholder="請輸入完整地址">
}

關鍵:透過 set,表單只需要一個 v-model,卻能同時更新多個資料屬性。


範例 3:過濾與統計(多層依賴)

export default {
  data() {
    return {
      products: [
        { name: '筆記型電腦', price: 35000, category: '電子' },
        { name: '滑鼠', price: 1200, category: '電子' },
        { name: '書桌', price: 8000, category: '家具' },
        // …更多資料
      ],
      filterCategory: '電子'
    }
  },
  computed: {
    // 先依類別過濾
    filteredProducts() {
      return this.products.filter(p => p.category === this.filterCategory)
    },
    // 再計算總金額
    totalPrice() {
      return this.filteredProducts.reduce((sum, p) => sum + p.price, 0)
    },
    // 最後產生可顯示的文字
    summary() {
      return `類別 ${this.filterCategory} 共有 ${this.filteredProducts.length} 件商品,總價 $${this.totalPrice}`
    }
  }
}

實務:在電商後台常用此方式快速切換類別統計,computed 的快取讓切換 UI 時不會重算所有商品。


範例 4:日期格式化(依賴外部函式)

import { format } from 'date-fns'   // npm 安裝 date-fns

export default {
  data() {
    return {
      rawDate: new Date()
    }
  },
  computed: {
    // 使用第三方函式格式化日期
    formattedDate() {
      // **注意**:format 本身是純函式,不會改變 rawDate
      return format(this.rawDate, 'yyyy/MM/dd HH:mm')
    }
  }
}

說明:即使 format 是外部函式,只要傳入的參數是響應式的(rawDate),computed 仍會正確追蹤變化。


範例 5:懶載入使用者資料(結合 watch)

export default {
  data() {
    return {
      userId: 2,
      userInfo: null,
      loading: false,
      error: null
    }
  },
  computed: {
    // 只要 userId 改變,就產生新的 Promise
    userPromise() {
      this.loading = true
      this.error = null
      return fetch(`https://jsonplaceholder.typicode.com/users/${this.userId}`)
        .then(r => r.ok ? r.json() : Promise.reject('Network error'))
    }
  },
  watch: {
    // 當 computed 回傳的 Promise 完成時,寫入 userInfo
    userPromise: {
      immediate: true,
      handler(promise) {
        promise
          .then(data => {
            this.userInfo = data
          })
          .catch(err => {
            this.error = err
          })
          .finally(() => {
            this.loading = false
          })
      }
    }
  }
}

最佳實踐:把「依賴變動才發送請求」的邏輯放在 computed,再用 watch 處理非同步結果,避免在 mounted 中寫冗長的條件判斷。


常見陷阱與最佳實踐

陷阱 說明 解法 / 最佳實踐
把副作用寫在 computed computed 應該是純函式,不能直接呼叫 API、改變資料或觸發 UI。 使用 watchwatchEffect 處理副作用。
忽略快取機制 當依賴是 物件/陣列,若只是變更內部屬性(如 push),computed 仍會偵測到,因 Vue 內部使用 Proxy。 若使用 非響應式 物件(如 Object.freeze),需要手動觸發更新。
computed 中使用大量計算 每次依賴變更都會重新執行,若演算法過於昂貴會影響效能。 把重度計算抽成 純函式,或使用 memoization(如 lodash.memoize)。
忘記 return 在 Options API 中,computed 必須回傳值,漏寫 return 會得到 undefined 確認每個 getter 都有 return,或使用簡寫語法 fullName() { return ... }
set 中直接改變依賴 set 內部又觸發另一個 computed,可能造成 無限循環 確保 set 只改變 source data,不要直接寫入其他 computed。

最佳實踐

  1. 保持純函式computed 只做「計算」不做「副作用」。
  2. 適度拆分:把大型計算拆成多個小的 computed,利用快取鏈提升效能。
  3. 使用 getter / setter 只在需要雙向綁定時,否則使用只讀形式即可。
  4. 命名一致:以 xxxListxxxCountxxxFormatted 等後綴明確表示回傳類型。
  5. 測試快取行為:開發時可在 getter 裡 console.log,觀察是否因不必要的依賴而頻繁執行。

實際應用場景

  1. 表單資料合併
    多個輸入欄位(如姓名、電話、地址)需要在提交前組成一個 JSON 物件,使用 computed 可以即時預覽合併結果,減少手動拼接的錯誤。

  2. 資料表格的分頁、排序與過濾

    • filteredData:根據搜尋關鍵字過濾
    • sortedData:根據欄位排序
    • pagedData:根據目前頁碼切割
      這三層 computed 可以保證每次只重新計算受影響的那一層。
  3. 國際化(i18n)字串
    依賴當前語系 (locale) 的 computed 可自動返回對應的翻譯文字,無需在模板裡寫繁雜的條件判斷。

  4. 圖表資料的即時轉換
    從原始 API 回傳的資料陣列,透過 computed 產生符合 Chart.js、ECharts 等套件所需的 labels / datasets 結構,讓圖表渲染保持同步且效能佳。

  5. 權限判斷
    依據使用者角色 (user.role) 計算出可操作的功能清單(allowedActions),在 UI 中直接以 v-if="allowedActions.includes('edit')" 控制顯示。


總結

  • computedOptions API 中用來產生「衍生」資料的核心工具,具備 快取依賴追蹤 兩大特性。
  • 只讀形式適合顯示或重複使用的資料;具 get/set 的雙向綁定則能簡化表單與資料同步的程式碼。
  • 多層依賴、外部函式、甚至懶載入非同步資料,都可以透過 computed 搭配 watch 來實現彈性且效能友好的解決方案。
  • 避免在 computed 中寫副作用、過度計算或無意的循環,遵守「純函式」原則與適當的命名規則,能讓程式碼更易維護。

掌握了以上概念與實務範例,你就能在 Vue3 的 Options API 中,利用 computed 建構出高效、可讀、易維護的前端應用。祝開發順利! 🚀