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.delete | set、deleteProperty 陷阱可检测 |
key in obj、for...in/Object.keys | 依赖不到新增的 key | has、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。