本文 AI 產出,尚未審核

Vue 3 響應式系統:基於 Proxy 的 Re‑activity


簡介

在 Vue 2 時代,響應式系統是以 Object.defineProperty 為核心實作的。雖然當時已經相當好用,但在面對深層巢狀物件、陣列變化以及 delete/add 屬性等操作時,仍會出現 偵測不到變化效能瓶頸 的問題。

Vue 3 以 ES6 Proxy 取代 Object.defineProperty,重新打造了更完整、更高效的響應式機制。透過 Proxy,Vue 能在 讀取、寫入、刪除、遍歷 等所有層面的操作上即時追蹤,讓開發者只需要關注資料本身,而不必再手動處理 Vue.setVue.delete 等輔助函式。

本文將從 概念實作範例常見陷阱最佳實踐 以及 實務應用 四個面向,深入淺出地說明 Vue 3 的 Proxy‑based reactivity,幫助初學者快速上手,同時提供中級開發者優化專案的思路。


核心概念

1. Proxy 與 Reflect

Proxy Reflect
目的 攔截目標物件的所有操作(get、set、deleteProperty、has…) 提供與原始操作相同的 API,讓 Proxy 內部可以「安全」地執行原始行為
使用方式 new Proxy(target, handler) Reflect.get(target, key, receiver) 等方法

Vue 3 的 reactive()ref()computed() 等 API,底層皆是透過 Proxy 包裝原始資料,並在 handler 中加入 依賴收集(dependency tracking)與 觸發更新(trigger)兩大核心步驟。

2. 依賴收集與觸發

  1. 依賴收集(track)

    • 當組件在渲染階段讀取某個屬性時(proxy.prop),Proxy 的 get 捕獲器會呼叫 track(target, key),把當前的 effect(即渲染函式)記錄在 target[key] 的依賴集合中。
  2. 觸發更新(trigger)

    • 當屬性被寫入或刪除時(proxy.prop = newValdelete proxy.prop),setdeleteProperty 捕獲器會呼叫 trigger(target, key),遍歷先前收集的 effect,重新執行以更新 UI。

:Vue 內部使用 Map<target, Map<key, Set<effect>>> 來管理依賴關係,這也是為什麼 Proxy 能在任意深度的巢狀結構上保持正確追蹤的原因。

3. reactiverefreadonly 的差異

API 目的 是否可變 是否深層代理
reactive(obj) 把普通物件轉成 可變深層 的 Proxy
ref(value) 包裝 原始值單一物件,返回一個擁有 .value 的 Ref ✅(可改 .value 只代理單層(若值為物件,內部會自動 reactive
readonly(obj) 產生 只讀 代理,寫入操作會在開發模式拋出警告
shallowReactive(obj) / shallowReadonly(obj) 淺層 代理,只追蹤第一層屬性 依 API 而定 ❌(子層不會被代理)

程式碼範例

以下示範 Vue 3 中最常見的 Proxy‑based reactivity 用法,從基礎到進階一步步說明。

1. 基本的 reactive 使用

import { reactive, effect } from 'vue'

const state = reactive({
  count: 0,
  user: {
    name: 'Alice',
    age: 30
  }
})

// effect 會在 state 任何屬性被讀取時收集依賴
effect(() => {
  console.log('count 變了:', state.count)
})

// 改變值會自動觸發 effect
state.count++          // console: count 變了: 1
state.user.age = 31   // 這裡不會觸發上面的 effect,因為它沒有讀取 user.age

重點effect 只會在它 讀取 的屬性被改變時重新執行,這正是 Vue 3 依賴追蹤的核心。

2. ref 包裝原始值

import { ref, effect } from 'vue'

const message = ref('Hello')

// effect 只會在 .value 被讀取時收集依賴
effect(() => {
  console.log('訊息是:', message.value)
})

message.value = 'Hi Vue 3'   // console: 訊息是: Hi Vue 3

技巧:在模板 ({{ message }}) 中直接使用 ref 時,Vue 內部會自動解包 .value,所以開發者在模板裡不需要寫 .value

3. 深層嵌套的 Proxy(自動遞迴)

import { reactive, effect } from 'vue'

const deepState = reactive({
  level1: {
    level2: {
      count: 0
    }
  }
})

effect(() => {
  console.log('深層 count:', deepState.level1.level2.count)
})

// 任意層級的寫入都會觸發 effect
deepState.level1.level2.count = 5   // console: 深層 count: 5

說明reactive 會在第一次存取子層物件時(deepState.level1.level2)自動返回一個新的 Proxy,實現 遞迴代理

4. readonly 防止意外變更

import { readonly, reactive } from 'vue'

const source = reactive({ name: 'Bob' })
const ro = readonly(source)

effect(() => {
  console.log('readonly name:', ro.name)
})

// 嘗試寫入會在開發模式拋出警告,且不會改變值
ro.name = 'Tom'   // 警告:Attempting to mutate readonly value
console.log(source.name) // 仍然是 Bob

實務:在大型專案中,常把 store 的 state 以 readonly 暴露給組件,保證只有特定的 mutation 函式能修改資料。

5. shallowReactiveshallowReadonly(效能優化)

import { shallowReactive, effect } from 'vue'

const shallow = shallowReactive({
  nested: { count: 0 },
  simple: 1
})

effect(() => {
  console.log('simple 變化:', shallow.simple)
})

// 只會追蹤第一層
shallow.simple = 2   // console: simple 變化: 2
shallow.nested.count = 10   // 不會觸發上面的 effect,因為 nested 本身不是 reactive

使用時機:當你確定子層物件不會在 UI 中直接被觀測,或是需要減少 Proxy 的建立成本時,shallowReactive 是很好的選擇。


常見陷阱與最佳實踐

陷阱 說明 解決方式 / 最佳實踐
直接修改 ref 內的物件屬性 ref({ a: 1 }) 後,ref.value.a = 2 不會觸發依賴(因為 ref 只追蹤 .value 本身) 使用 reactive 包裝物件,或在修改後手動呼叫 ref.value = { ...ref.value } 重新賦值
setup 之外使用 reactive Vue 的響應式 API 必須在 Vue 內部的生命週期(如 setupcomponent)內呼叫,否則無法正確注入依賴追蹤上下文 確保所有 reactive/ref 呼叫都在 setup()watchEffectcomputed 等函式內
大量深層物件一次性替換 直接 state = newObj 會失去原有的 Proxy,導致 UI 不再更新 使用 Object.assign(state, newObj)state.someProp = newObj.someProp,保持原始 Proxy 不變
在模板中使用 Object.keysfor...in 直接遍歷 這些操作會觸發 Proxy 的 ownKeys,但 Vue 並不會自動收集依賴 使用 computed(() => Object.keys(state)) 包裹,或改用 v-for 直接遍歷已經是 reactive 的陣列/物件
watch 中直接改變被監聽的值 可能導致無限迴圈(watch 觸發 → 改變 → 再次觸發) 使用 watchEffect 或在 watch 的回呼函式中加入 flush: 'post',或在修改前先檢查是否真的變更

最佳實踐小結

  1. 盡量使用 reactive 處理複雜物件,僅在需要單一值或原始值時才使用 ref
  2. 保持 Proxy 的引用不變:盡量不要把整個 reactive 物件重新指派給另一個變數,這會失去跟蹤。
  3. 使用 computed 包裝衍生資料,避免在模板或 watch 中直接寫大量計算邏輯。
  4. 在大型專案中導入 readonly,將 store 的 state 只讀化,確保資料流的單向性。
  5. 根據需求選擇 shallow 版本:若只需要追蹤第一層,使用 shallowReactive 可減少 Proxy 建立成本,提升效能。

實際應用場景

1. 建立輕量級的全域狀態管理(Pinia / Vuex 替代)

// store.js
import { reactive, readonly } from 'vue'

const _state = reactive({
  todos: [],
  filter: 'all'
})

export const useStore = () => ({
  // 只讀狀態,外部組件只能透過 actions 改變
  state: readonly(_state),

  // mutation-like actions
  addTodo(text) {
    _state.todos.push({ id: Date.now(), text, done: false })
  },
  toggleTodo(id) {
    const todo = _state.todos.find(t => t.id === id)
    if (todo) todo.done = !todo.done
  },
  setFilter(value) {
    _state.filter = value
  }
})

說明:透過 reactive + readonly,我們只需要幾行程式碼就能完成一個可觀測且安全的全域 store,且所有 UI 會自動根據 state 的變化重新渲染。

2. 表單資料雙向綁定與即時驗證

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

export default {
  setup() {
    const form = reactive({
      email: '',
      password: ''
    })

    const errors = reactive({
      email: '',
      password: ''
    })

    const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/

    // 即時驗證
    watch(() => form.email, (newVal) => {
      errors.email = emailPattern.test(newVal) ? '' : 'Email 格式不正確'
    })

    const canSubmit = computed(() => {
      return form.email && form.password && !errors.email && !errors.password
    })

    return { form, errors, canSubmit }
  }
}

重點watch 只會在 form.email 改變時觸發,computed 會根據 errors 自動重新計算 canSubmit,全部都是透過 Proxy 完成的即時反應。

3. 動態生成表格欄位(深層結構)

import { reactive, effect } from 'vue'

const tableData = reactive({
  rows: [
    { id: 1, name: 'Alice', address: { city: 'Taipei', zip: '100' } },
    { id: 2, name: 'Bob',   address: { city: 'Kaohsiung', zip: '800' } }
  ]
})

// 只要任何欄位變化,都會重新渲染表格
effect(() => {
  console.table(tableData.rows.map(r => ({
    ID: r.id,
    Name: r.name,
    City: r.address.city,
    ZIP: r.address.zip
  })))
})

// 更新深層屬性
tableData.rows[0].address.city = 'New Taipei'   // 表格即時更新

實務:在資料表、樹狀結構或是樞紐分析等需要大量深層變更的場景,Vue 3 的 Proxy 能自動追蹤任意深度,開發者不必自行寫遞迴監聽程式。


總結

Vue 3 以 ES6 Proxy 為基礎,重新構築了響應式系統,使得:

  • 深層結構 能被完整追蹤,無需手動 Vue.setVue.delete
  • 依賴收集觸發更新 的流程更清晰,開發者可以透過 effectwatchcomputed 等 API 自由組合。
  • readonlyshallowReactive 等變體提供了更彈性的使用方式,讓效能與安全性可以依需求微調。

掌握了 Proxy‑based reactivity 後,你可以:

  1. 快速構建全域狀態管理(如 Pinia 內部的實作),只需幾行 reactive/readonly 代碼。
  2. 寫出即時驗證、表單雙向綁定,讓 UI 與資料保持同步。
  3. 在大型、深層資料結構中保持高效更新,避免因手動追蹤而產生的錯誤與效能問題。

最後,別忘了在開發環境保持 devtools 開啟,它能幫助你直觀觀察每個 Proxy 的依賴關係,快速定位問題。祝你在 Vue 3 的世界裡玩得開心,寫出更乾淨、更高效的前端程式碼! 🚀