本文 AI 產出,尚未審核

Vue3 課程 – 響應式系統(Reactivity System)

主題:Computed 的 Lazy Evaluation 機制


簡介

在 Vue 3 中,computed 是建立衍生狀態(derived state)的核心工具。它不僅讓我們可以把複雜的計算邏輯抽離出來,保持模板簡潔,也因為 lazy evaluation(懶評估) 的特性,使得效能表現大幅提升。

對於任何需要根據多個 reactive 來源計算結果的場景(例如表單驗證、列表過濾、統計資訊),了解 computed 為何只在「需要」時才重新計算,才能寫出既正確高效的程式碼。本文將從概念、實作細節、常見陷阱到實務應用,完整說明 Vue 3 中 computed 的 lazy evaluation 機制,幫助初學者快速上手,同時提供中階開發者優化應用的技巧。


核心概念

1. Computed 的基本原理

computed 本質上是一個 getter 函式,它會在以下兩個時機產生值:

  1. 首次存取:第一次讀取 computed.value 時,Vue 會執行 getter,並把結果快取起來。
  2. 依賴變更且被再次存取:只要 getter 內部所依賴的 reactive 狀態(refreactiveprops 等)發生變化,Vue 會標記此 computed 為 dirty,但不會立即重新計算。等到下次讀取時才會重新執行。

這樣的 惰性求值 能避免不必要的計算,尤其在 UI 更新頻繁、計算成本高的情況下,效能提升顯著。

2. 內部實作:Dep、Effect 與 Scheduler

Vue 3 的響應式系統以 Proxy 包裝資料,並透過 Dep(依賴集合)ReactiveEffect 追蹤變化。computed 的實作大致如下:

步驟 說明
創建 ReactiveEffect 包裹 getter,並在執行時收集依賴(即哪些 reactive 屬性被讀取)。
標記 dirty 當依賴的 reactive 觸發 triggercomputed 的 effect 會把 dirty = true,但不執行 getter。
在 getter 中判斷 當外部讀取 computed.value,若 dirty 為 true,重新執行 getter 並更新快取值,最後把 dirty 變回 false。

這個流程保證了 只在需要時才重新計算,而不是每次依賴變更時即刻執行。

3. Computed 與 Watch 的差異

功能 computed watch
目的 產生衍生的、可直接在模板或程式中使用的值 監聽變化執行副作用(如 API 請求、手動 DOM 操作)
執行時機 懶評估:只有在讀取時才計算 即時:依賴變更即觸發回調
返回值 包含 .value 的 Ref 物件 無返回值,回調函式自行處理

了解這兩者的差異,有助於在開發時正確選擇工具,避免不必要的效能浪費。

4. 多層 Computed 的傳播

computed 可以相互嵌套,形成「計算鏈」。每個節點仍遵循 lazy evaluation,只有最終讀取時才會向上觸發計算。例如:

const a = ref(1)
const b = computed(() => a.value + 1)          // b 依賴 a
const c = computed(() => b.value * 2)          // c 依賴 b

a 改變時,b 會被標記為 dirty,c 也會被標記為 dirty。只有在 c.value 被讀取時,b 先重新計算,再由 c 計算最終結果。


程式碼範例

以下提供 5 個實用範例,展示 lazy evaluation 在不同情境下的表現與注意事項。每段程式碼均加上說明註解,方便讀者快速理解。

範例 1:基本的懶評估

import { ref, computed } from 'vue'

const count = ref(0)

// 只有在第一次取值時才會執行 getter
const double = computed(() => {
  console.log('computed: double 被執行')
  return count.value * 2
})

console.log('第一次讀取 double')
console.log(double.value) // 觸發 getter,印出 0

console.log('再次讀取 double(未變更)')
console.log(double.value) // 使用快取值,不再執行 getter

count.value++               // 變更依賴,標記 dirty
console.log('變更後再次讀取 double')
console.log(double.value) // 再次執行 getter,印出 2

重點:只有在 double.value 被讀取且依賴已變更時,getter 才會重新執行。

範例 2:多層 Computed(計算鏈)

import { ref, computed } from 'vue'

const price = ref(100)
const taxRate = ref(0.1)

// 第一步:計算稅額
const tax = computed(() => {
  console.log('計算稅額')
  return price.value * taxRate.value
})

// 第二步:計算含稅總價
const total = computed(() => {
  console.log('計算含稅總價')
  return price.value + tax.value
})

console.log(total.value) // 觸發 tax、total
price.value = 200        // 只標記 dirty,未立即計算
console.log(total.value) // 再次觸發 tax、total,印出新結果

說明:當 price 變更時,taxtotal 都被標記為 dirty。只有在 total.value 被讀取時,才會依序重新計算 tax 再計算 total

範例 3:在模板中使用 Computed

<template>
  <div>
    <input v-model.number="age" placeholder="輸入年齡" />
    <p>您在 {{ retirementAge }} 歲退休。</p>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const age = ref(30)
const retirementAge = computed(() => {
  console.log('計算退休年齡')
  return age.value + 35   // 假設退休年齡是 35 歲後
})
</script>

觀察:僅當 age 改變且模板重新渲染時,retirementAge 的 getter 會被觸發一次,避免不必要的計算。

範例 4:與 watch 結合的場景

import { ref, computed, watch } from 'vue'

const items = ref([{ price: 10 }, { price: 20 }])

// 計算總金額(懶評估)
const total = computed(() => {
  console.log('計算總金額')
  return items.value.reduce((sum, i) => sum + i.price, 0)
})

// 當總金額變更時,觸發副作用(例如發送 API)
watch(total, (newVal, oldVal) => {
  console.log(`總金額改變:${oldVal} → ${newVal}`)
  // sendUpdateToServer(newVal)
})

說明watch 只會在 total 真正重新計算(即依賴變更且被讀取)後才觸發,避免因頻繁的資料變更而產生過多的 API 呼叫。

範例 5:自訂 Scheduler 讓 Computed 更加「延遲」

Vue 允許在 computed 內部傳入 scheduler,自訂何時重新計算。以下示範將計算延遲到下一個微任務(Promise.resolve().then(...)):

import { ref, computed } from 'vue'

const num = ref(1)

const lazy = computed(
  () => {
    console.log('執行 lazy getter')
    return num.value * 10
  },
  {
    // scheduler 會在依賴變更時被呼叫
    scheduler: () => {
      console.log('scheduler 被觸發,排入微任務')
      Promise.resolve().then(() => {
        // 手動觸發重新計算
        lazy.effect.run()
      })
    }
  }
)

console.log(lazy.value) // 首次執行
num.value = 2           // 只觸發 scheduler,不立即執行 getter
console.log('變更後立即讀取')
console.log(lazy.value) // 仍是舊值,因為 getter 尚未跑
// 微任務完成後,lazy 會在下次讀取時得到新值
setTimeout(() => {
  console.log('微任務後再次讀取')
  console.log(lazy.value) // 這次會是 20
}, 0)

重點:自訂 scheduler 能讓開發者控制計算的時機,進一步優化渲染排程或與第三方庫(如動畫框架)協同。


常見陷阱與最佳實踐

陷阱 可能的問題 解決方案或最佳實踐
在 computed 中直接修改 reactive 會產生「副作用」且可能造成無限遞迴 保持純函式:computed 應只返回值,若需改變狀態,使用 watcheffect
誤以為 computed 永遠即時更新 依賴變更後若沒有讀取 .value,結果不會更新 確保使用點:在模板或程式碼中真的需要時才讀取;若需要立即觸發,可手動讀取一次
過度嵌套 computed 產生過深的依賴鏈,除非必要會影響除錯 適度抽離:把複雜邏輯拆成多個小的 computed,或使用普通函式作為輔助
在 computed 中使用非 reactive 變數 變化不會被追蹤,導致結果不一致 只依賴 reactive:若需要外部常數,直接在 getter 中使用;若需要變化,請轉成 ref
忘記在 setup 中返回 computed 模板無法存取,導致錯誤 返回或暴露:在 setup()return { myComputed },或使用 <script setup> 自動暴露

最佳實踐小結

  1. 保持純粹:computed 的 getter 應只做「計算」不做「副作用」。
  2. 只在需要時讀取:利用懶評估的特性,避免在不必要的地方頻繁讀取。
  3. 適度使用 watch:當需要在值變更時執行副作用,使用 watch 而非在 computed 內部改變狀態。
  4. 利用 scheduler:在高頻更新(如滑鼠拖曳、滾動)時,透過自訂 scheduler 把計算延後,以減少渲染次數。
  5. 測試依賴:開發時可在 getter 裡 console.log,觀察何時被觸發,確保依賴正確收集。

實際應用場景

場景 為何使用 Computed(lazy)
表單驗證 多個欄位的驗證規則相互依賴,使用 computed 產生 isFormValid,只有在表單提交前才計算一次。
列表過濾與分頁 把原始資料 (items) 與過濾條件 (searchKeyword) 結合成 filteredItems,懶評估避免每次鍵入都重新渲染整個列表。
金額統計 交易資料大量變更時,使用 computed 計算總收入、平均值等指標,只在顯示儀表板時才重新計算。
動態樣式 根據視窗寬度或主題顏色計算 CSS 變數,懶評估確保只有在相關屬性變更時才觸發 style 重算。
跨組件共享狀態 透過 Pinia/ Vuex 的 getter(實際上是 computed)提供衍生狀態,讓多個組件共享同一個懶評估結果。

示例:以下示範一個「商品列表」的過濾與分頁,使用兩層 computed(過濾 → 分頁)來最大化懶評估效益。

import { ref, computed } from 'vue'

const products = ref([
  /* 大量商品資料 */
])

const search = ref('')
const currentPage = ref(1)
const pageSize = 10

// 1️⃣ 過濾條件
const filtered = computed(() => {
  console.log('執行過濾')
  const kw = search.value.trim().toLowerCase()
  if (!kw) return products.value
  return products.value.filter(p => p.name.toLowerCase().includes(kw))
})

// 2️⃣ 分頁結果(依賴 filtered)
const paged = computed(() => {
  console.log('計算分頁')
  const start = (currentPage.value - 1) * pageSize
  return filtered.value.slice(start, start + pageSize)
})

// 在模板中直接使用 paged

在使用者輸入搜尋關鍵字時,只有 filtered 會被重新計算;而切換分頁只會觸發 pagedfiltered 仍保持快取,極大減少不必要的遍歷。


總結

Vue 3 的 computed 透過 lazy evaluation(懶評估)與 依賴追蹤,在保證 正確性 的同時,最大化 效能

  • 核心概念:首次讀取才計算、依賴變更時僅標記 dirty、下次讀取時才重新計算。
  • 實作要點:利用 ReactiveEffectDepscheduler,實現「只在需要時」的計算策略。
  • 最佳實踐:保持 getter 純函式、適度使用 watch、必要時自訂 scheduler。
  • 實務應用:表單驗證、列表過濾、金額統計、動態樣式、跨組件共享狀態等,都能從懶評估中受益。

掌握了 computed 的 lazy evaluation 機制,你就能在 Vue 3 應用中寫出 更快、更易維護 的程式碼,為使用者提供流暢的互動體驗。祝你在 Vue 的世界裡玩得開心,持續探索更進階的響應式技巧!