Vue3 課程 – 響應式系統(Reactivity System)
主題:Computed 的 Lazy Evaluation 機制
簡介
在 Vue 3 中,computed 是建立衍生狀態(derived state)的核心工具。它不僅讓我們可以把複雜的計算邏輯抽離出來,保持模板簡潔,也因為 lazy evaluation(懶評估) 的特性,使得效能表現大幅提升。
對於任何需要根據多個 reactive 來源計算結果的場景(例如表單驗證、列表過濾、統計資訊),了解 computed 為何只在「需要」時才重新計算,才能寫出既正確又高效的程式碼。本文將從概念、實作細節、常見陷阱到實務應用,完整說明 Vue 3 中 computed 的 lazy evaluation 機制,幫助初學者快速上手,同時提供中階開發者優化應用的技巧。
核心概念
1. Computed 的基本原理
computed 本質上是一個 getter 函式,它會在以下兩個時機產生值:
- 首次存取:第一次讀取
computed.value時,Vue 會執行 getter,並把結果快取起來。 - 依賴變更且被再次存取:只要 getter 內部所依賴的 reactive 狀態(
ref、reactive、props等)發生變化,Vue 會標記此 computed 為 dirty,但不會立即重新計算。等到下次讀取時才會重新執行。
這樣的 惰性求值 能避免不必要的計算,尤其在 UI 更新頻繁、計算成本高的情況下,效能提升顯著。
2. 內部實作:Dep、Effect 與 Scheduler
Vue 3 的響應式系統以 Proxy 包裝資料,並透過 Dep(依賴集合) 與 ReactiveEffect 追蹤變化。computed 的實作大致如下:
| 步驟 | 說明 |
|---|---|
| 創建 ReactiveEffect | 包裹 getter,並在執行時收集依賴(即哪些 reactive 屬性被讀取)。 |
| 標記 dirty | 當依賴的 reactive 觸發 trigger,computed 的 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變更時,tax與total都被標記為 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 應只返回值,若需改變狀態,使用 watch 或 effect |
| 誤以為 computed 永遠即時更新 | 依賴變更後若沒有讀取 .value,結果不會更新 |
確保使用點:在模板或程式碼中真的需要時才讀取;若需要立即觸發,可手動讀取一次 |
| 過度嵌套 computed | 產生過深的依賴鏈,除非必要會影響除錯 | 適度抽離:把複雜邏輯拆成多個小的 computed,或使用普通函式作為輔助 |
| 在 computed 中使用非 reactive 變數 | 變化不會被追蹤,導致結果不一致 | 只依賴 reactive:若需要外部常數,直接在 getter 中使用;若需要變化,請轉成 ref |
忘記在 setup 中返回 computed |
模板無法存取,導致錯誤 | 返回或暴露:在 setup() 中 return { myComputed },或使用 <script setup> 自動暴露 |
最佳實踐小結
- 保持純粹:computed 的 getter 應只做「計算」不做「副作用」。
- 只在需要時讀取:利用懶評估的特性,避免在不必要的地方頻繁讀取。
- 適度使用
watch:當需要在值變更時執行副作用,使用watch而非在 computed 內部改變狀態。 - 利用
scheduler:在高頻更新(如滑鼠拖曳、滾動)時,透過自訂 scheduler 把計算延後,以減少渲染次數。 - 測試依賴:開發時可在 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 會被重新計算;而切換分頁只會觸發 paged,filtered 仍保持快取,極大減少不必要的遍歷。
總結
Vue 3 的 computed 透過 lazy evaluation(懶評估)與 依賴追蹤,在保證 正確性 的同時,最大化 效能。
- 核心概念:首次讀取才計算、依賴變更時僅標記 dirty、下次讀取時才重新計算。
- 實作要點:利用
ReactiveEffect、Dep與scheduler,實現「只在需要時」的計算策略。 - 最佳實踐:保持 getter 純函式、適度使用
watch、必要時自訂 scheduler。 - 實務應用:表單驗證、列表過濾、金額統計、動態樣式、跨組件共享狀態等,都能從懶評估中受益。
掌握了 computed 的 lazy evaluation 機制,你就能在 Vue 3 應用中寫出 更快、更易維護 的程式碼,為使用者提供流暢的互動體驗。祝你在 Vue 的世界裡玩得開心,持續探索更進階的響應式技巧!