为什么 Vue3 用 Proxy,而不是 defineProperty?
一、从一个最小需求开始假设你要实现一个能力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 的做法Proxyconst proxy new Proxy(target, { get(target, key) {}, set(target, key, value) {}, deleteProperty(target, key) {} })关键变化从“劫持属性” → “劫持对象行为”也就是基于操作的拦截operation-based六、Proxy 如何解决这些问题1. 动态属性问题obj.a 1 // 可以监听 delete obj.a // 可以监听因为拦截的是setdeleteProperty2. 数组问题arr[0] 100 arr.length 0全部可以监听。原因Proxy 可以拦截多种操作而不仅仅是 get / set3. 性能问题最关键Proxy 的策略是访问到哪一层 → 才代理哪一层示例state.user // 才对 user 做 proxy这叫惰性代理lazy reactive相比 Vue2不需要初始化递归按需代理这一点是性能提升的核心七、一个更本质的对比方案本质defineProperty劫持“数据”Proxy劫持“行为”换句话说Vue2 是“数据驱动”Vue3 是“操作驱动”八、工程层面的差异Vue2this.list[0] 100 // 不更新 this.list.splice(0,1,100) // 才更新Vue3this.list[0] 100 // 正常更新说明Vue2 需要开发者配合框架Vue3 框架适配开发者九、Proxy 的代价1. 兼容性问题Proxy 无法被 polyfillIE 完全不支持这也是 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 中的许多内置对象如Map、Set、Date、Promise依赖底层的内部槽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在处理Map、Set时必须要在内部重写它们的get、set、add等方法手动将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本质是在