关于 Vue 响应式: Vue 2 使用 Object.defineProperty 无法监听数组下标的变化,也无法监听对象新增属性,为什么?Vue 3 的 Proxy 是如何解决这两个问题的?
纠正一个概念: Vue 2 使用的 Object.defineProperty 其实也是通过 get() 和 set() 来拦截的。所以,区别不在于“有没有 get/set”,而在于 拦截的层级不同。
我们可以用一句话概括核心区别:
- Vue 2 (
Object.defineProperty): 是 “属性级” 拦截。它必须针对对象的每一个具体的 Key 进行单独的定义。 - Vue 3 (
Proxy): 是 “对象级” 拦截。它直接代理了整个对象,不管里面有多少个 Key,或者未来会增加什么 Key,它都能管得着。
下面我们用更形象的例子和代码来拆解一下:
1. Vue 2:尽职的“门口保安” (Object.defineProperty)
Object.defineProperty 就像是你给大楼里的每一个房间(属性)都派了一个保安。
为什么监听不到对象新增属性?
当你初始化 Vue 2 时,它会遍历 data 里的所有属性(比如 name, age),给这几个已知的房间派了保安。
// Vue 2 初始化伪代码
let obj = { name: 'Gemini' };
// 只要你访问 obj.name,就会经过这个拦截
Object.defineProperty(obj, 'name', {
get() { return val; },
set(newVal) { updateView(); }
});
问题来了:
如果你突然在对象上加了一个新属性 obj.sex = 'male'(盖了一个新房间),因为初始化的时候这个房间不存在,所以没有派保安去守着。
当你修改 obj.sex 时,Vue 根本不知道,所以视图不会更新。
- 补救措施: 这就是为什么 Vue 2 需要
Vue.set(obj, 'sex', 'male')。它的作用就是手动补派一个保安过去。
为什么监听不到数组下标变化?
这是一个常见的误区。
技术上讲,Object.defineProperty 是可以 监听数组下标的(因为数组下标本质上就是 Key)。
// 技术上是可以做到的
Object.defineProperty(arr, '0', { ... });
Object.defineProperty(arr, '1', { ... });
但是,Vue 2 为什么放弃了这么做?
因为性能消耗太大了!
如果你有一个数组,里面有 10,000 个元素。Vue 就必须在初始化时循环 10,000 次,给每一个下标都加上 get/set。而且如果数组之后 push 了新元素,还得再给新元素加拦截。这会导致初始化巨慢无比。
所以,Vue 2 为了性能,选择了一个“权宜之计”:它重写(Monkey Patch) 了数组的 7 个方法(push, pop, shift, unshift, splice, sort, reverse)。
- 当你调用
arr.push(1)时,Vue 知道数组变了。 - 但当你直接
arr[0] = 1时,Vue 没办法拦截,只能“装瞎”。
2. Vue 3:全能的“大楼网关” (Proxy)
Proxy 不再是针对具体的“房间”,而是在大楼的门口设了一个唯一的总安检闸机。
不管你是要去 name 房间,还是去 age 房间,甚至是去一个昨天还没建好、今天刚建好的 sex 房间,你都必须经过这个大门口。
let obj = { name: 'Gemini', arr: [1, 2] };
// Vue 3 的代理逻辑
const proxy = new Proxy(obj, {
// 拦截读取
get(target, key, receiver) {
console.log(`监听到你正在读取: ${key}`);
return Reflect.get(target, key, receiver);
},
// 拦截设置
set(target, key, value, receiver) {
console.log(`监听到你正在设置: ${key} 为 ${value}`);
// 关键点:这里不需要关心 key 是否已经存在
return Reflect.set(target, key, value, receiver);
}
});
Proxy 如何解决那两个问题?
-
新增属性:
当你执行proxy.sex = 'male'时,触发了set拦截器。此时参数key就是'sex'。Proxy不需要提前知道'sex'存不存在,反正只要你往对象里塞东西,它就能拦截到。这就是为什么 Vue 3 不需要Vue.set。 -
数组下标变化:
当你执行proxy.arr[0] = 99时:- 这也是一个
set操作。 target是数组。key是'0'。Proxy会直接拦截到这次修改。
不需要像 Vue 2 那样去循环遍历数组的每一个下标,Proxy 天生就能拦截对象(和数组)的所有操作。
- 这也是一个
3. 深度对比总结表
| 特性 | Object.defineProperty (Vue 2) | Proxy (Vue 3) |
|---|---|---|
| 拦截层级 | 属性级 (针对具体的 Key) | 对象级 (针对整个 Object) |
| 新增属性 | ❌ 无法感知 (需用 Vue.set) | ✅ 天然感知 (触发 set 陷阱) |
| 删除属性 | ❌ 无法感知 (需用 Vue.delete) | ✅ 天然感知 (触发 deleteProperty 陷阱) |
| 数组下标 | ❌ 性能原因放弃支持 (无法拦截 arr[0]=1) | ✅ 天然支持 |
| Map/Set | ❌ 不支持 | ✅ 支持 |
| 兼容性 | ✅ IE9+ | ❌ 不兼容 IE (这是 Vue 3 最大的痛点) |
4. 补充一个进阶点 (Reflect)
你在问题里提到了 get() 和 set(),在 Vue 3 的源码中,Proxy 通常是和 Reflect 配合使用的(就像我上面的代码一样)。
为什么要用 Reflect?
主要是为了修正 this 的指向。
如果对象内部有 Getter 访问了 this.xxx,如果不配合 Reflect,这个 this 可能会指向原始对象而不是代理对象,导致依赖收集失败
更多推荐



所有评论(0)