为什么 Vue3 用 Proxy,而不是 defineProperty?

不想废话了,直接进入主题。这不是一个“API 升级”的问题,而是响应式模型发生了变化


一、从一个最小需求开始

假设你要实现一个能力:

const state = {
  user: { name: '张三' }
}

state.user.name = '李四'
// 页面自动更新

本质需求只有一个:

监听数据变化,并触发副作用(更新视图)


二、Vue2 的做法:defineProperty

核心实现方式:

Object.defineProperty(obj, 'name', {
  get() {
    // 收集依赖
  },
  set(val) {
    // 触发更新
  }
})

这里有一个关键点:

defineProperty 只能作用在“属性”上

也就是说,它的模型是:

基于属性的劫持(property-based)


三、问题不是功能不够,而是模型不对

下面这些问题,其实都可以归因到这一点。


1. 无法监听新增 / 删除属性

const obj = {}
obj.a = 1  // 不会触发更新

必须这样:

Vue.set(obj, 'a', 1)

原因很简单:

defineProperty 只能拦截“已存在的属性”


2. 数组无法统一处理

arr[0] = 100   // 无法监听
arr.length = 0 // 无法监听

Vue2 的做法是:

  • 重写 push / splice / pop 等方法
  • 做一层“补丁逻辑”

本质是:

不是统一拦截,而是针对特殊结构做兼容处理


3. 初始化成本高(核心问题)

defineProperty 的实现必须:

deepObject → 递归遍历 → 每个属性都 defineProperty

这意味着:

  • 初始化成本高
  • 深层对象全部劫持(即使不会访问)

本质问题:

一次性全量劫持


四、关键结论

到这里应该得出一个判断:

defineProperty 的问题不是“能力弱”,而是“抽象层级错误”

它试图:

监听“数据本身”

但前端真正需要的是:

监听“对数据的操作”


五、Vue3 的做法:Proxy

const proxy = new Proxy(target, {
  get(target, key) {},
  set(target, key, value) {},
  deleteProperty(target, key) {}
})

关键变化:

从“劫持属性” → “劫持对象行为”

也就是:

基于操作的拦截(operation-based)


六、Proxy 如何解决这些问题


1. 动态属性问题

obj.a = 1        // 可以监听
delete obj.a     // 可以监听

因为拦截的是:

  • set
  • deleteProperty

2. 数组问题

arr[0] = 100
arr.length = 0

全部可以监听。

原因:

Proxy 可以拦截多种操作,而不仅仅是 get / set


3. 性能问题(最关键)

Proxy 的策略是:

访问到哪一层 → 才代理哪一层

示例:

state.user  // 才对 user 做 proxy

这叫:

惰性代理(lazy reactive)

相比 Vue2:

  • 不需要初始化递归
  • 按需代理

这一点是性能提升的核心


七、一个更本质的对比

方案 本质
defineProperty 劫持“数据”
Proxy 劫持“行为”

换句话说:

Vue2 是“数据驱动”
Vue3 是“操作驱动”


八、工程层面的差异

Vue2

this.list[0] = 100  // 不更新
this.list.splice(0,1,100) // 才更新

Vue3

this.list[0] = 100  // 正常更新

说明:

Vue2 需要开发者配合框架
Vue3 框架适配开发者


九、Proxy 的代价

1. 兼容性问题

Proxy 无法被 polyfill:

  • IE 完全不支持

这也是 Vue3 放弃 IE 的原因之一。


2. 调试复杂

console.log(state)

输出的是 Proxy,而不是原始对象。


3. 与某些库存在兼容问题

例如:

1. 深拷贝(丢失代理或报错)

深拷贝通常会针对原始对象的数据进行拷贝。如果直接对一个 Proxy 对象进行深拷贝,拷贝出来的新对象会丢失代理(Proxy)特性,变成一个普通对象。

示例代码:

const target = { name: "Alice" };
const proxy = new Proxy(target, {
  set(obj, prop, val) {
    console.log(`拦截:设置 ${prop} = ${val}`);
    obj[prop] = val;
    return true;
  }
});

// 1. 测试原始代理
proxy.name = "Bob"; // 控制台输出:拦截:设置 name = Bob

// 2. 使用 JSON 深拷贝
const clone1 = JSON.parse(JSON.stringify(proxy));
clone1.name = "Charlie"; // 没有任何输出,Proxy 拦截器丢失了!

// 3. 使用原生 structuredClone 深拷贝
const clone2 = structuredClone(proxy);
clone2.name = "David"; // 同样没有任何输出,变成了一个普通对象

代价分析: 当你在 Vue3 中把一个 reactive 对象放进深拷贝函数中,拷贝出来的对象将失去响应式。


2. 类实例(原生对象/内部槽 This 指向异常)

JavaScript 中的许多内置对象(如 MapSetDatePromise)依赖底层的内部槽(Internal Slots),例如 Map 依赖 [[MapData]]。Proxy 是无法代理内部槽的。当通过 Proxy 调用这些对象的方法时,方法内部的 this 指向了 Proxy,导致找不到内部槽而报错。

示例代码:

const map = new Map();
const proxyMap = new Proxy(map, {});

// 直接操作 target 没问题
map.set('key1', 'value1'); 

// 通过 proxy 操作会直接崩溃
proxyMap.set('key2', 'value2'); 
// 报错:TypeError: Method Map.prototype.set called on incompatible receiver [object Object]

代价分析: 这就是为什么 Vue3 的 reactive 在处理 MapSet 时,必须要在内部重写它们的 getsetadd 等方法,手动将 this 绑定回原始对象(target)。


3. 私有字段(# 语法报错)

ES2022 引入了类的私有属性(以 # 开头)。私有属性是严格绑定到类的具体实例上的。如果你对一个包含私有属性的类实例进行代理,当调用实例方法访问私有属性时,会因为 this 指向 Proxy 而导致报错。

示例代码:

class User {
  #password = "123456"; // 私有字段

  checkPassword() {
    // 这里的 this 必须是 User 的真实实例
    return this.#password;
  }
}

const user = new User();
const proxyUser = new Proxy(user, {});

// 直接调用没问题
console.log(user.checkPassword()); // "123456"

// 通过 Proxy 调用崩溃
console.log(proxyUser.checkPassword()); 
// 报错:TypeError: Cannot read private member #password from an object whose class did not declare it

解决方法/代价: 如果必须在使用私有字段的类上使用 Proxy,你必须在 Proxy 的 get 拦截器中,将方法的 this 强行 bind 回目标对象:

const proxyUserFixed = new Proxy(user, {
  get(target, prop, receiver) {
    const value = Reflect.get(...arguments);
    // 如果是函数,强行绑定 this 为原始对象 target
    return typeof value === 'function' ? value.bind(target) : value;
  }
});

console.log(proxyUserFixed.checkPassword()); // "123456" (正常工作了)

十、结论

Vue2 使用 defineProperty,本质是在:

用“属性劫持”模拟响应式

Vue3 使用 Proxy,本质是在:

用“语言级能力”表达响应式

最终变化不是优化,而是:

响应式系统的建模方式改变


如果有什么问题或者可以改进的地方,欢迎评论区留言,大家一起讨论。

posted @ 2026-04-29 10:56  幼儿园技术家  阅读(9)  评论(0)    收藏  举报