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.$setVue.set,説明已踩中響應式盲區,升級 Vue3 可一次性消除。
  • 新 Vue3 項目:優先使用 reactive 管理對象,ref 管理原始值;避免把 reactive 包進 ref,防止雙重代理帶來的額外開銷。
  • 性能調優:藉助 markRawshallowReactive 把大列表、第三方庫實例標記為非響應式,減少追蹤壓力。

結語

Vue3 的響應式不是「打補丁」,而是「換引擎」。Proxy 讓「增刪改查」全部可追蹤,WeakMap 讓依賴收集更精準。這次底層躍遷,徹底解決了 Vue2 的響應式難題,也為 Vue3 的性能優化奠定了基礎。