前端知库

defineProperty 和 Proxy 实现的响应式有什么区别?

Vue 2 用 Object.defineProperty 在初始化时递归为已有属性定义 getter/setter;Vue 3 用 Proxy 代理整个对象,访问到嵌套对象时才惰性代理,能拦截属性增删、in、遍历、数组下标与 length,并通过专门的处理器支持 Map/Set。依赖由 effect、track、trigger 管理。

参考答案

简答

  • Vue 2 已于 2023-12-31 停止维护,Vue 2 部分以 2.7 为准,Vue 3 部分以 3.5 为准。
  • defineProperty 作用于单个已有属性,Vue 2 初始化时要递归遍历整个 data;Proxy 作用于整个对象,Vue 3 在 get 读到嵌套对象时才为它创建代理。
  • Proxy 的 deleteProperty、has、ownKeys 等陷阱让 Vue 3 能检测属性增删、in、遍历以及数组下标和 length,Vue.set/Vue.delete 因此被移除。
  • Map、Set、WeakMap、WeakSet 由单独的集合处理器拦截 get、set、add 等方法;Vue 2 不支持它们的响应式。
  • Vue 3 中 effect 执行时读取属性调用 track 登记依赖,写入时 trigger 找到依赖的 effect 重新执行或调度。

能力边界对比

Vue 2(defineProperty)Vue 3(Proxy)
新增/删除属性检测不到,需 Vue.set/Vue.deleteset、deleteProperty 陷阱可检测
key in obj、for...in/Object.keys依赖不到新增的 keyhas、ownKeys 陷阱可追踪
数组下标赋值、改 length检测不到,靠重写 7 个变异方法直接检测
Map/Set不支持collectionHandlers 支持
原对象原地修改,加上 getter/setter 和 __ob__不修改原对象,返回代理;修改原对象不会触发更新
浏览器ES5,支持 IE9+Proxy 无法 polyfill,Vue 3 不支持 IE11

官方 Vue 3 文档说明:Vue 2 由于浏览器支持限制只用 getter/setter;Vue 3 对 reactive 对象使用 Proxy,对 ref 使用 getter/setter。迁移指南也写明 $set/$delete 在基于 Proxy 的变更检测下不再需要。

初始化与性能

  • Vue 2 在创建实例时一次性递归转换所有嵌套属性,数据越大初始化越慢,即使有些属性从未被渲染。大型只读数据常用 Object.freeze 跳过转换。
  • Vue 3 的 reactive() 只代理顶层;get 陷阱返回值是对象时才调用 reactive(res),并用 WeakMap(proxyMap)缓存,同一原对象总是得到同一个代理。
  • 代价是每次属性访问都经过代理陷阱。大数据可用 shallowRef/shallowReactive 或 markRaw 跳过深层代理。

这里不给出具体倍数,两者的性能差异取决于数据规模和访问模式。

effect、track、trigger

Vue 3.5 的结构:

  • 全局 targetMap: WeakMap<target, Map<key, Dep>> 保存"对象的某个 key 被哪些 effect 依赖"。
  • track(target, type, key):当前有活跃 effect 时,找到或创建对应的 Dep 并登记。
  • trigger(target, type, key):取出对应的 Dep 通知订阅者;新增或删除属性时还会触发遍历用的 ITERATE_KEY,数组改 length 时会触发受影响的下标。
  • 组件渲染、computed、watch 都建立在 effect 之上,组件更新通过调度器进入异步队列。

更完整的过程见《Vue 3 响应式依赖是怎样收集和触发的?》;ref 与 reactive 的选择见《Vue 3 中 ref 和 reactive 有什么区别?》。

示例

JavaScript
// 用 Node 运行,vue@3.5.x
const { reactive, effect, toRaw } = require('vue');
const s = reactive({ a: { b: 1 }, list: [1, 2, 3], m: new Map() });
const log = [];
effect(() => log.push(`${'c' in s} ${s.list.length} ${s.m.get('k')}`));

s.c = 1; // 新增属性
s.list.length = 1; // 改 length
s.m.set('k', 2); // Map
delete s.c; // 删除属性
console.log(log);
// [ 'false 3 undefined', 'true 3 undefined', 'true 1 undefined', 'true 1 2', 'false 1 2' ]

console.log(s.a === s.a); // true,嵌套代理被缓存
console.log(toRaw(s).a === toRaw(s.a)); // true,原对象没有被替换

effect 来自 @vue/reactivity,同步执行,所以每次修改都会立即记录一行;组件渲染则会经过调度器合并。

常见追问

  • Vue 2.7 有 reactive(),它也是 Proxy 吗?不是。2.7 的 reactive 仍基于 defineProperty,限制与 Vue 2 相同。
  • 解构 reactive 对象为什么会失去响应式?解构得到的是普通值,绕过了代理的 get。