跳至主要內容
Skip to content

Vue computed 的實現原理 ​

computed 是 Vue 響應式系統裡最常被問的 API 之一。它看起來像一個函數,使用起來像一個值,但背後其實結合了 依賴收集、快取、惰性求值與失效通知。

一句話回答:computed 會把 getter 包裝成一個特殊的 ref。它第一次讀取 .value 時才執行 getter,執行時收集依賴;依賴沒變時直接回傳快取值,依賴變了才標記為 dirty,等下次讀取時重新計算。


一、 必考觀念 ​

1. computed 解決什麼問題? ​

很多畫面值不是直接存在 state 裡,而是由 state 推導出來。

vue
<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,你可能會寫成:

typescript
const fullName = ref('')

watch([firstName, lastName], () => {
  fullName.value = `${firstName.value} ${lastName.value}`
})

這樣多了一份可變狀態,容易出現同步問題。computed 的價值就是:讓衍生狀態保持 declarative,只描述它如何被推導。

2. computed 有快取 ​

這是 computed 和 methods 最大差異。

vue
<template>
  <p>{{ fullName }}</p>
  <p>{{ fullName }}</p>
  <p>{{ fullName }}</p>
</template>
typescript
const fullName = computed(() => {
  console.log('computed run')
  return `${firstName.value} ${lastName.value}`
})

如果 firstName 和 lastName 沒變,同一次渲染裡讀很多次 fullName,getter 不會每次都重新執行,而是回傳快取值。

methods 則不同:

typescript
function getFullName() {
  console.log('method run')
  return `${firstName.value} ${lastName.value}`
}

模板每次呼叫 getFullName() 都會執行函數。

3. computed 是惰性求值 ​

computed 不會在建立當下立刻計算。

typescript
const fullName = computed(() => {
  console.log('run')
  return `${firstName.value} ${lastName.value}`
})

如果沒有人讀:

typescript
fullName.value

getter 就不需要執行。

這叫 lazy evaluation,惰性求值。它的好處是:如果某個 computed 沒被用到,就不必浪費計算。

4. computed 本質上是一個 ref ​

Composition API 裡:

typescript
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

在 <script setup> 裡讀取時要用:

typescript
fullName.value

這是因為 computed 回傳的是一個 computed ref。

簡化心智模型:

typescript
const fullName = {
  get value() {
    // 讀取時才計算或回傳快取
  },
}

所以它既像值,又能被其他 effect 追蹤。


二、 實現原理 ​

1. 最小版 computed ​

先看一個非常簡化的 computed:

typescript
function computed(getter) {
  let value
  let dirty = true

  return {
    get value() {
      if (dirty) {
        value = getter()
        dirty = false
      }

      return value
    },
  }
}

這能解釋兩件事:

  • 第一次讀取才執行 getter。
  • 第二次讀取會回傳快取。

但它還缺一件事:依賴變更時,怎麼讓快取失效?

2. 加上依賴追蹤 ​

Vue 的響應式核心是 track 和 trigger:

text
讀取 reactive/ref
-> track 收集目前 active effect

修改 reactive/ref
-> trigger 通知依賴它的 effect

computed 的 getter 執行時,也會成為一個 reactive effect。當 getter 裡讀到 firstName.value、lastName.value,這些 ref 會把 computed effect 收集起來。

3. 依賴變更時先標記 dirty ​

computed 的關鍵不是依賴一變就立刻重新計算,而是先讓快取失效。

簡化版:

typescript
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
    },
  }
}

流程是:

  1. 第一次讀 computed.value,執行 getter 並快取結果。
  2. getter 執行期間讀到依賴,依賴會收集 computed effect。
  3. 依賴改變時,不一定馬上重新算,而是把 computed 標記為 dirty。
  4. 下次有人讀 computed.value,才重新執行 getter。

這就是「惰性 + 快取 + 失效」。

4. computed 也需要被別人追蹤 ​

computed 不只會追蹤自己的依賴,它自己也可以成為別人的依賴。

typescript
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

watchEffect(() => {
  console.log(fullName.value)
})

watchEffect 讀取 fullName.value 時,會追蹤 fullName 這個 computed ref。

當 firstName 改變:

text
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 / depsTailcomputed 自己依賴了哪些 dep
flags標記 dirty、tracking、running 等狀態
globalVersion用於快速判斷全域響應式版本是否有變
refreshComputed在讀取 .value 時判斷是否需要重新計算

你不需要在面試中背每個欄位,但要能說出核心:

computed 會保存快取值,讀取時會追蹤外部 effect;getter 執行時會追蹤內部依賴;依賴改變後先標記 dirty,等下一次讀取再透過 refresh 類邏輯重新計算。

6. 可寫 computed ​

預設 computed 是 readonly:

typescript
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

fullName.value = 'Ada Byron' // 不應該這樣寫

如果需要寫入,可以提供 get 和 set:

typescript
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 ​

比較computedmethods
是否快取是,依賴不變就回傳快取否,每次呼叫都執行
適合衍生狀態事件處理、需要參數的操作
使用方式像值一樣讀取像函數一樣呼叫

適合 computed:

typescript
const totalPrice = computed(() => {
  return items.value.reduce((sum, item) => sum + item.price, 0)
})

適合 methods:

typescript
function formatPrice(price: number) {
  return `$${price.toFixed(2)}`
}

2. computed vs watch ​

比較computedwatch
用途產生衍生值觀察變化並執行副作用
是否應該有副作用不應該可以
回傳值有通常沒有
常見場景篩選列表、格式化資料、計算總價API 請求、同步 localStorage、操作 DOM

computed 應該保持純粹:

typescript
const filteredUsers = computed(() => {
  return users.value.filter(user => user.active)
})

watch 適合副作用:

typescript
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、事件處理、生命週期或資料層邏輯裡。

錯誤方向:

typescript
const users = computed(async () => {
  return await fetchUsers()
})

比較合理:

typescript
watch(keyword, async (value) => {
  users.value = await fetchUsers(value)
})

Q4:computed 可以依賴另一個 computed 嗎? ​

可以。

typescript
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 裡修改狀態,可能造成難以預測的更新循環。

不建議:

typescript
const total = computed(() => {
  count.value++
  return count.value * 2
})

computed 的目標是推導值,不是觸發流程。

Q6:computed 和 template 裡直接寫表達式有什麼差? ​

簡單表達式可以直接寫在模板:

vue
{{ firstName + ' ' + lastName }}

但如果邏輯變長、會重複使用、或計算成本較高,就應該抽成 computed:

typescript
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

這樣可讀性更好,也能利用 computed 快取。


五、 實作題 ​

題目:手寫一個簡化版 computed ​

下面是一個極簡響應式系統的 computed 心智模型:

typescript
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:

typescript
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 最大的差別。

延伸閱讀 ​