Vue computed 的實現原理
computed 是 Vue 響應式系統裡最常被問的 API 之一。它看起來像一個函數,使用起來像一個值,但背後其實結合了 依賴收集、快取、惰性求值與失效通知。
一句話回答:computed 會把 getter 包裝成一個特殊的 ref。它第一次讀取 .value 時才執行 getter,執行時收集依賴;依賴沒變時直接回傳快取值,依賴變了才標記為 dirty,等下次讀取時重新計算。
一、 必考觀念
1. computed 解決什麼問題?
很多畫面值不是直接存在 state 裡,而是由 state 推導出來。
<script setup lang="ts">
import { computed, ref } from 'vue'
const firstName = ref('Ada')
const lastName = ref('Lovelace')
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
</script>fullName 不應該另外手動維護,因為它可以由 firstName 和 lastName 推導。
如果不用 computed,你可能會寫成:
const fullName = ref('')
watch([firstName, lastName], () => {
fullName.value = `${firstName.value} ${lastName.value}`
})這樣多了一份可變狀態,容易出現同步問題。computed 的價值就是:讓衍生狀態保持 declarative,只描述它如何被推導。
2. computed 有快取
這是 computed 和 methods 最大差異。
<template>
<p>{{ fullName }}</p>
<p>{{ fullName }}</p>
<p>{{ fullName }}</p>
</template>const fullName = computed(() => {
console.log('computed run')
return `${firstName.value} ${lastName.value}`
})如果 firstName 和 lastName 沒變,同一次渲染裡讀很多次 fullName,getter 不會每次都重新執行,而是回傳快取值。
methods 則不同:
function getFullName() {
console.log('method run')
return `${firstName.value} ${lastName.value}`
}模板每次呼叫 getFullName() 都會執行函數。
3. computed 是惰性求值
computed 不會在建立當下立刻計算。
const fullName = computed(() => {
console.log('run')
return `${firstName.value} ${lastName.value}`
})如果沒有人讀:
fullName.valuegetter 就不需要執行。
這叫 lazy evaluation,惰性求值。它的好處是:如果某個 computed 沒被用到,就不必浪費計算。
4. computed 本質上是一個 ref
Composition API 裡:
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})在 <script setup> 裡讀取時要用:
fullName.value這是因為 computed 回傳的是一個 computed ref。
簡化心智模型:
const fullName = {
get value() {
// 讀取時才計算或回傳快取
},
}所以它既像值,又能被其他 effect 追蹤。
二、 實現原理
1. 最小版 computed
先看一個非常簡化的 computed:
function computed(getter) {
let value
let dirty = true
return {
get value() {
if (dirty) {
value = getter()
dirty = false
}
return value
},
}
}這能解釋兩件事:
- 第一次讀取才執行 getter。
- 第二次讀取會回傳快取。
但它還缺一件事:依賴變更時,怎麼讓快取失效?
2. 加上依賴追蹤
Vue 的響應式核心是 track 和 trigger:
讀取 reactive/ref
-> track 收集目前 active effect
修改 reactive/ref
-> trigger 通知依賴它的 effectcomputed 的 getter 執行時,也會成為一個 reactive effect。當 getter 裡讀到 firstName.value、lastName.value,這些 ref 會把 computed effect 收集起來。
3. 依賴變更時先標記 dirty
computed 的關鍵不是依賴一變就立刻重新計算,而是先讓快取失效。
簡化版:
function computed(getter) {
let value
let dirty = true
const effect = createReactiveEffect(getter, {
scheduler() {
dirty = true
triggerComputedSubscribers()
},
})
return {
get value() {
trackComputedSubscribers()
if (dirty) {
value = effect.run()
dirty = false
}
return value
},
}
}流程是:
- 第一次讀
computed.value,執行 getter 並快取結果。 - getter 執行期間讀到依賴,依賴會收集 computed effect。
- 依賴改變時,不一定馬上重新算,而是把 computed 標記為 dirty。
- 下次有人讀
computed.value,才重新執行 getter。
這就是「惰性 + 快取 + 失效」。
4. computed 也需要被別人追蹤
computed 不只會追蹤自己的依賴,它自己也可以成為別人的依賴。
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
watchEffect(() => {
console.log(fullName.value)
})watchEffect 讀取 fullName.value 時,會追蹤 fullName 這個 computed ref。
當 firstName 改變:
firstName trigger
-> computed 被標記 dirty
-> computed 通知依賴它的 effect
-> watchEffect 重新執行
-> 讀 fullName.value
-> computed 重新計算所以 computed 既是 subscriber,也是 dep provider:
- 它訂閱
firstName、lastName。 - 它也被
watchEffect、component render effect 訂閱。
5. Vue 3.5 的實作重點
本專案目前使用 Vue 3.5.x。Vue 3.5 的 reactivity 實作裡,computed 由 ComputedRefImpl 管理,核心概念包含:
| 概念 | 作用 |
|---|---|
_value | 快取 computed 的目前值 |
dep | 追蹤誰讀取了這個 computed |
deps / depsTail | computed 自己依賴了哪些 dep |
flags | 標記 dirty、tracking、running 等狀態 |
globalVersion | 用於快速判斷全域響應式版本是否有變 |
refreshComputed | 在讀取 .value 時判斷是否需要重新計算 |
你不需要在面試中背每個欄位,但要能說出核心:
computed 會保存快取值,讀取時會追蹤外部 effect;getter 執行時會追蹤內部依賴;依賴改變後先標記 dirty,等下一次讀取再透過 refresh 類邏輯重新計算。
6. 可寫 computed
預設 computed 是 readonly:
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
fullName.value = 'Ada Byron' // 不應該這樣寫如果需要寫入,可以提供 get 和 set:
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(value: string) {
const [first, last] = value.split(' ')
firstName.value = first
lastName.value = last
},
})可寫 computed 的原理是:讀取走 getter,寫入 .value 時呼叫 setter。
三、 computed vs methods vs watch
1. computed vs methods
| 比較 | computed | methods |
|---|---|---|
| 是否快取 | 是,依賴不變就回傳快取 | 否,每次呼叫都執行 |
| 適合 | 衍生狀態 | 事件處理、需要參數的操作 |
| 使用方式 | 像值一樣讀取 | 像函數一樣呼叫 |
適合 computed:
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => sum + item.price, 0)
})適合 methods:
function formatPrice(price: number) {
return `$${price.toFixed(2)}`
}2. computed vs watch
| 比較 | computed | watch |
|---|---|---|
| 用途 | 產生衍生值 | 觀察變化並執行副作用 |
| 是否應該有副作用 | 不應該 | 可以 |
| 回傳值 | 有 | 通常沒有 |
| 常見場景 | 篩選列表、格式化資料、計算總價 | API 請求、同步 localStorage、操作 DOM |
computed 應該保持純粹:
const filteredUsers = computed(() => {
return users.value.filter(user => user.active)
})watch 適合副作用:
watch(keyword, async (value) => {
results.value = await search(value)
})四、 追問題庫
Q1:computed 什麼時候會重新計算?
當它依賴的響應式資料發生變化,computed 會被標記為 dirty。下一次有人讀取 .value 時,才會重新執行 getter。
如果依賴沒有變,多次讀取會直接回傳快取。
Q2:computed 沒被使用會執行嗎?
不會。
computed 是 lazy 的。建立 computed 不代表立刻執行 getter,只有讀取 .value 或在模板中使用它時才會求值。
Q3:computed 裡可以做 API 請求嗎?
不建議。
computed 應該是純粹的同步衍生值。API 請求是副作用,應該放在 watch、事件處理、生命週期或資料層邏輯裡。
錯誤方向:
const users = computed(async () => {
return await fetchUsers()
})比較合理:
watch(keyword, async (value) => {
users.value = await fetchUsers(value)
})Q4:computed 可以依賴另一個 computed 嗎?
可以。
const subtotal = computed(() => {
return items.value.reduce((sum, item) => sum + item.price, 0)
})
const total = computed(() => {
return subtotal.value * 1.05
})total 會追蹤 subtotal,而 subtotal 會追蹤 items。當 items 改變時,subtotal dirty,進一步通知 total 也需要更新。
Q5:computed 的 getter 為什麼不建議修改其他狀態?
因為 getter 應該是純函數。如果在 computed 裡修改狀態,可能造成難以預測的更新循環。
不建議:
const total = computed(() => {
count.value++
return count.value * 2
})computed 的目標是推導值,不是觸發流程。
Q6:computed 和 template 裡直接寫表達式有什麼差?
簡單表達式可以直接寫在模板:
{{ firstName + ' ' + lastName }}但如果邏輯變長、會重複使用、或計算成本較高,就應該抽成 computed:
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})這樣可讀性更好,也能利用 computed 快取。
五、 實作題
題目:手寫一個簡化版 computed
下面是一個極簡響應式系統的 computed 心智模型:
let activeEffect = null
class Dep {
subscribers = new Set()
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach(effect => effect())
}
}
function ref(value) {
const dep = new Dep()
return {
get value() {
dep.depend()
return value
},
set value(next) {
value = next
dep.notify()
},
}
}加入 computed:
function computed(getter) {
let value
let dirty = true
const dep = new Dep()
const effect = () => {
dirty = true
dep.notify()
}
return {
get value() {
dep.depend()
if (dirty) {
const previous = activeEffect
activeEffect = effect
value = getter()
activeEffect = previous
dirty = false
}
return value
},
}
}這個版本很粗糙,但能呈現核心:
- computed 有自己的快取
value。 - computed 有 dirty 狀態。
- getter 執行時會收集依賴。
- 依賴變化後不直接計算,而是標記 dirty 並通知使用者。
真實 Vue 會更複雜,包含 effect stack、dep cleanup、job batching、版本號、遞迴保護等。
六、 資深視角
1. computed 是資料建模工具
computed 不只是效能優化 API。更重要的是它讓「衍生狀態」不需要被手動同步。
如果某個值可以由其他狀態推導,就優先考慮 computed,而不是再建立一個 ref 並用 watch 維護。
2. 避免過度使用 computed
不是所有東西都要 computed。
不一定需要 computed:
- 只使用一次的簡單模板表達式。
- 事件處理裡才需要的臨時計算。
- 需要傳參數的格式化函數。
- 有副作用的非同步流程。
computed 適合的是可被重複讀取的同步衍生值。
3. computed 的成本來自依賴與快取管理
computed 有快取,但不是免費。它需要追蹤依賴、維護 dep、判斷 dirty、通知訂閱者。
大多數情況下這些成本很小,但如果你建立大量 computed,或者 computed 裡做很重的資料處理,就仍然可能造成效能問題。
這時應該思考:
- 是否能縮小資料量?
- 是否能拆分 computed?
- 是否需要虛擬列表或分頁?
- 是否應該把昂貴計算移到資料層或 worker?
總結
| 問題 | 面試回答重點 |
|---|---|
| computed 是什麼 | 基於響應式依賴的衍生狀態 |
| 是否立即執行 | 否,lazy,讀取時才計算 |
| 是否有快取 | 有,依賴不變就回傳快取 |
| 依賴變了怎麼辦 | 標記 dirty,下一次讀取重新計算 |
| 本質 | 一個特殊的 computed ref |
| 與 methods 差異 | computed 有快取,methods 每次呼叫都執行 |
| 與 watch 差異 | computed 產生值,watch 處理副作用 |
一句完整的面試回答可以是:
Vue 的
computed本質是一個特殊的 ref。它會把 getter 包裝成 reactive effect,但不會立刻執行,而是在第一次讀取.value時才求值。getter 執行期間會收集依賴,結果會被快取;當依賴的響應式資料變化時,computed 不一定立即重新計算,而是先標記 dirty,並通知依賴它的 effect。下一次讀取.value時,如果 dirty 才重新執行 getter,否則直接回傳快取值。這也是 computed 和 methods 最大的差別。