从Vue2的Object.defineProperty到Vue3的Proxy,数据劫持进化论

阿乐
阿乐 管理员 黑卡会员
发布于 2026-09-10 16:44 ·8 浏览 ·0 回复

响应式系统是 Vue 的灵魂,而「数据劫持」则是这套灵魂的底层实现。当我们在模板里写下 `{{ count }}`,在方法里执行 `this.count++`,界面随之刷新——这背后发生的一切,都依赖于框架对数据的监听能力。

Vue2 和 Vue3 在这个问题上给出了截然不同的答案:一个用 `Object.defineProperty` 逐个属性地改写 getter/setter,一个用 `Proxy` 直接代理整个对象。这不是简单的 API 替换,而是一次从「打补丁」到「换地基」的架构升级。

Vue2:用 Object.defineProperty 逐个击破

Vue2 初始化数据时,会递归遍历 `data` 中的每个属性,调用 `Object.defineProperty` 把它们改造成存取器属性:

function defineReactive(obj, key, val) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      if (Dep.target) dep.depend() // 收集依赖
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      dep.notify() // 派发更新
    }
  })
}

读取时收集依赖,写入时通知视图更新,这套机制在当年足够优雅,但它有三个绕不开的硬伤。

第一,无法感知属性的新增和删除。 `defineProperty` 只能劫持已经存在的属性,`obj.newKey = 1` 或 `delete obj.key` 都不会触发 setter,所以 Vue2 不得不提供 `Vue.set` / `Vue.delete` 这两个特殊 API,让开发者手动补一刀。

第二,数组是半残的。 出于性能考虑,Vue2 没有对数组的每个索引做劫持,而是重写了 `push`、`pop`、`splice` 等七个方法。这意味着 `arr[0] = 'x'` 和 `arr.length = 0` 这类操作同样无法响应。

第三,初始化成本高。 递归遍历意味着无论数据是否会被用到,所有层级都必须提前处理完毕。一个深层嵌套的大对象,光是初始化就要付出可观的代价。

Vue3:Proxy 带来的整体代理

`Proxy` 是 ES6 提供的元编程能力,它不需要修改原对象,而是在对象外层架设一层「拦截网」,可以捕获 get、set、deleteProperty、has、ownKeys 等十三种操作:

function createReactiveObject(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key) // 依赖收集
      const res = Reflect.get(target, key, receiver)
      return isObject(res) ? reactive(res) : res // 惰性递归
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      if (oldValue !== value) trigger(target, key) // 派发更新
      return result
    },
    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key)
      const result = Reflect.deleteProperty(target, key)
      if (hadKey) trigger(target, key)
      return result
    }
  })
}

对比之下,Vue2 的三个痛点被逐一化解。新增和删除属性会被 `set`、`deleteProperty` 正常拦截,`Vue.set` 成了历史名词;数组的索引赋值和 `length` 修改也不再是盲区;而最关键的是惰性递归——`get` 拦截中只有当访问到的值确实是对象时,才返回它的响应式代理,没被用到的深层数据永远不会被处理。

变化的不只是 API

如果只把这次演进理解为「换了个劫持手段」,就低估了它的意义。Vue3 的依赖收集结构从 Vue2 的 `Dep` 换成了三层嵌套的 `WeakMap → Map → Set`,天然支持以对象为键,内存回收也更友好;配合 `Reflect` 保证 receiver 传递正确,`this` 指向不会在代理链中错乱;再加上对 `Map`、`Set`、`WeakMap`、`WeakSet` 的原生支持,Vue2 时代那些「集合类型不响应」的坑被一次性填平。

性能上也有得有失:Proxy 的访问速度确实比 getter 略慢,但惰性代理让初始化阶段省下大量递归开销,在大型对象场景下反而更快。

Proxy 也不是银弹

最现实的问题摆在最前面:`Proxy` 无法被 polyfill,IE 全系不支持。这也是 Vue3 干脆放弃 IE11 的直接原因。`Object.defineProperty` 虽然能力有限,但它属于 ES5,兼容性至今仍是它的护城河。

另一个容易被忽视的细节是原始对象与代理对象的区分。执行 `reactive(obj)` 后,`obj` 和返回的 proxy 是两个东西,Vue3 内部用 `__v_raw` 标记和 `toRaw` API 来应对,使用不当就会出现「改了数据但不更新」的困惑。

写在最后

从 `Object.defineProperty` 到 `Proxy`,Vue 完成了一次从「逐属性打补丁」到「整体拦截」的思维转变。前者受限于语言能力的边界,需要框架用各种约定和补丁去填补;后者站在新的语言特性上,把曾经需要绕行的坑变成了直路。

这也提醒我们:技术选型从来不是单纯的新旧之争。`Proxy` 更强,但代价是放弃兼容性;`defineProperty` 更老,却在很长一段时间里支撑起了整个前端生态的响应式想象。理解它们各自的边界,比记住谁取代了谁更重要。

本文转载自 阿乐技术社区,原文地址:https://www.leleweb.cn/thread-212.html
转载请注明出处,版权归原作者所有。
他们都看过 1 人浏览过
不能说的秘密

全部回复 0

还没有回复,来抢沙发~