Vue3 響應式革命
Vue2 驗證了「響應式驅動視圖」的威力,卻在數組索引、屬性增刪等場景留下無法追蹤的死角。Vue3 並未縫縫補補,而是把整座大廈的地基——數據攔截機制——徹底替換為 Proxy。本文沿着「攔截 → 創建 → 收集」三條鏈路,帶你讀懂這一次底層躍遷的全部細節。
一、攔截:從點到面的語義升級
1.Vue2 的「定點攔截」
Object.defineProperty 只能劫持已存在的屬性。當業務代碼 obj.newKey = 1 時,這條新增路徑對依賴系統完全不可見,於是官方只能額外暴露 Vue.set / vm.$set 作為補丁。
2.Vue3 的「整面攔截」
Proxy 把「對象」視為一個整體,任何對屬性的 讀取、寫入、刪除、遍歷 乃至 原型鏈讀取 都能被捕獲。
const p = new Proxy(obj, {
get(target, key, receiver) { /* 讀 */ },
set(target, key, value, receiver) { /* 寫 */ },
deleteProperty(target, key) { /* 刪 */ }
})
新增 key 不再逃逸,數組索引與 length 的變化自然落入監聽範圍,徹底告別 $set。
二、創建:ref 與 reactive 的分工
雖然 Proxy 能力更強,但「原始值」無法被代理。Vue3 用 RefImpl 與 ReactiveImpl 兩套實現互補:
- ref 負責原始值(Number、String、Boolean)——內部用
RefImpl包裹,讀/寫時觸發自定義 getter/setter;當值是對象時再遞歸交給reactive。 - reactive 負責對象/數組——直接返回 Proxy,攔截全部操作。
源碼片段(精簡):
class RefImpl<T> {
get value() {
track(this, 'value') // 收集
return this._value
}
set value(newVal) {
this._value = toReactive(newVal)
trigger(this, 'value') // 派發
}
}
function reactive(target: object) {
return createReactiveObject(target, mutableHandlers)
}
toReactive 判斷傳入值是否為對象,是則繼續包 Proxy,否則原樣返回,形成一條「層層代理,按需終止」的鏈。
三、收集:從 Watcher 樹到副作用圖
1.Vue2 的 Watcher + Dep
每個響應式屬性擁有一個 Dep,Dep 裏存着若干 Watcher(通常是一個組件渲染函數)。屬性變化 → 通知所有 Watcher → 組件級重渲染。
粒度:組件級別。
2.Vue3 的副作用圖
Vue3 不再關心「是哪個組件」,而是關心「哪個副作用函數」。數據結構是一張 WeakMap → Map → Set 的三級表:
- 第一級 WeakMap:鍵是響應式對象,值是第二級 Map;
- 第二級 Map:鍵是屬性名,值是第三級 Set;
- 第三級 Set:存儲所有依賴該屬性的
effect函數。
當屬性值改變時,只觸發 精確到函數 的重新執行,粒度從組件級降到函數級,更新範圍更小,性能更高。
四、工程實踐的遷移
- Vue2 項目:若觀察到大量
this.$set或Vue.set,説明已踩中響應式盲區,升級 Vue3 可一次性消除。 - 新 Vue3 項目:優先使用
reactive管理對象,ref管理原始值;避免把reactive包進ref,防止雙重代理帶來的額外開銷。 - 性能調優:藉助
markRaw或shallowReactive把大列表、第三方庫實例標記為非響應式,減少追蹤壓力。
結語
Vue3 的響應式不是「打補丁」,而是「換引擎」。Proxy 讓「增刪改查」全部可追蹤,WeakMap 讓依賴收集更精準。這次底層躍遷,徹底解決了 Vue2 的響應式難題,也為 Vue3 的性能優化奠定了基礎。