Vue 3 響應式系統:基於 Proxy 的 Re‑activity
簡介
在 Vue 2 時代,響應式系統是以 Object.defineProperty 為核心實作的。雖然當時已經相當好用,但在面對深層巢狀物件、陣列變化以及 delete/add 屬性等操作時,仍會出現 偵測不到變化 或 效能瓶頸 的問題。
Vue 3 以 ES6 Proxy 取代 Object.defineProperty,重新打造了更完整、更高效的響應式機制。透過 Proxy,Vue 能在 讀取、寫入、刪除、遍歷 等所有層面的操作上即時追蹤,讓開發者只需要關注資料本身,而不必再手動處理 Vue.set、Vue.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. 依賴收集與觸發
依賴收集(track)
- 當組件在渲染階段讀取某個屬性時(
proxy.prop),Proxy 的get捕獲器會呼叫track(target, key),把當前的 effect(即渲染函式)記錄在target[key]的依賴集合中。
- 當組件在渲染階段讀取某個屬性時(
觸發更新(trigger)
- 當屬性被寫入或刪除時(
proxy.prop = newVal、delete proxy.prop),set或deleteProperty捕獲器會呼叫trigger(target, key),遍歷先前收集的 effect,重新執行以更新 UI。
- 當屬性被寫入或刪除時(
註:Vue 內部使用
Map<target, Map<key, Set<effect>>>來管理依賴關係,這也是為什麼 Proxy 能在任意深度的巢狀結構上保持正確追蹤的原因。
3. reactive、ref、readonly 的差異
| 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. shallowReactive 與 shallowReadonly(效能優化)
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 內部的生命週期(如 setup、component)內呼叫,否則無法正確注入依賴追蹤上下文 |
確保所有 reactive/ref 呼叫都在 setup()、watchEffect、computed 等函式內 |
| 大量深層物件一次性替換 | 直接 state = newObj 會失去原有的 Proxy,導致 UI 不再更新 |
使用 Object.assign(state, newObj) 或 state.someProp = newObj.someProp,保持原始 Proxy 不變 |
在模板中使用 Object.keys、for...in 直接遍歷 |
這些操作會觸發 Proxy 的 ownKeys,但 Vue 並不會自動收集依賴 |
使用 computed(() => Object.keys(state)) 包裹,或改用 v-for 直接遍歷已經是 reactive 的陣列/物件 |
在 watch 中直接改變被監聽的值 |
可能導致無限迴圈(watch 觸發 → 改變 → 再次觸發) | 使用 watchEffect 或在 watch 的回呼函式中加入 flush: 'post',或在修改前先檢查是否真的變更 |
最佳實踐小結
- 盡量使用
reactive處理複雜物件,僅在需要單一值或原始值時才使用ref。 - 保持 Proxy 的引用不變:盡量不要把整個 reactive 物件重新指派給另一個變數,這會失去跟蹤。
- 使用
computed包裝衍生資料,避免在模板或watch中直接寫大量計算邏輯。 - 在大型專案中導入
readonly,將 store 的 state 只讀化,確保資料流的單向性。 - 根據需求選擇 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.set/Vue.delete。 - 依賴收集 與 觸發更新 的流程更清晰,開發者可以透過
effect、watch、computed等 API 自由組合。 readonly、shallowReactive等變體提供了更彈性的使用方式,讓效能與安全性可以依需求微調。
掌握了 Proxy‑based reactivity 後,你可以:
- 快速構建全域狀態管理(如 Pinia 內部的實作),只需幾行
reactive/readonly代碼。 - 寫出即時驗證、表單雙向綁定,讓 UI 與資料保持同步。
- 在大型、深層資料結構中保持高效更新,避免因手動追蹤而產生的錯誤與效能問題。
最後,別忘了在開發環境保持 devtools 開啟,它能幫助你直觀觀察每個 Proxy 的依賴關係,快速定位問題。祝你在 Vue 3 的世界裡玩得開心,寫出更乾淨、更高效的前端程式碼! 🚀