Vue3 为什么用 Proxy 代替 Object.defineProperty
Vue3 用 Proxy
代替 Vue2 用的 Object.defineProperty
来实现响应式系统。
首先熟悉一下 Proxy 和 Object.defineProperty 的基本使用。
Object.defineProperty
的局限性
1. 无 法监听数组索引的直接修改
通过索引直接设置数组元素(如 arr[0] = 1
)或修改数组长度(arr.length = 0
),不会触发 setter 函数。
const arr = [1, 2, 3];
Object.defineProperty(arr, '0', {
get() {
console.log('读取索引 0');
return this._value;
},
set(newVal) {
console.log('设置索引 0');
this._value = newVal;
}
});
arr[0] = 100; // 触发 setter
arr[3] = 4; // 不会触发任何监听(新增索引)
arr.length = 0; // 不会触发任何监听(修改 length)
Vue2 通过重写数组方法(如 push、pop 等)实现响应式。
2. 无法检测对象属性的添加或删除
动态新增的属性或删除属性无法被监听。因为 Object.defineProperty 只能劫持已定义的属性,对新属性 或删除操作无感知。
const obj = { a: 1 };
Object.defineProperty(obj, 'a', {
get() {
console.log('读取a');
return this._a;
},
set(newVal) {
console.log('设置a');
this._a = newVal;
}
});
obj.a = 2; // 触发 setter
obj.b = 3; // 不会触发任何监听(新增属性)
delete obj.a; // 不会触发任何监听(删除属性)
Vue2 提供了
Vue.set
和Vue.delete
方法,手动处理。
3.需要递归处理嵌套对象
若对象属性是另一个对象,修改嵌套对象的属性时,外层 setter 不会触发。
Object.defineProperty 只能劫持当前层属性,需递归遍历所有嵌套对象并为其属性设置响应式。这样在初始化深度遍历大型对象时可能会导致性能损耗。
const data = { nested: { value: 1 } };
let _nested = data.nested;
// 劫持外层对象 "nested"
Object.defineProperty(data, 'nested', {
get() {
console.log('读取嵌套对象');
return _nested; // 返回内部存储的嵌套对象
},
set(newVal) {
console.log('设置嵌套对象');
_nested = newVal; // 更新内部存储的嵌套对象
}
});
// 修改嵌套对象的属性
data.nested.value = 2; // 不会触发外层对象的 setter
data.nested = { value: 3 }; // 会触发外层对象的 setter
只有直接对 data.nested 整体赋值才会触发外层对象的 setter。data.nested.value = 2
仅仅是修改了 _nested
对象的 value 属性,而 _nested
本身未被重新赋值,因此外层 setter 不会触发。
必须为嵌套对象内部的属性也定义 getter/setter,即递归劫持所有层级的属性,才能监听嵌套属性的修改。
4. 不支持 Map/Set 等数据结构
无法劫持 ES6 的 Map
、Set
、WeakMap
等数据结构的变化。
这些数据结构通过方法(如 map.set()
)操作数据,而非属性访问,Object.defineProperty 无法拦截方法调用。
示例 1:覆盖单个实例的 set 方法
const map = new Map();
// 手动覆盖当前实例的 set 方法
Object.defineProperty(map, 'set', {
value: function (key, val) {
console.log('手动注入的 set 方法');
return Map.prototype.set.call(this, key, val);
}
});
map.set('key', 'value'); // 输出log
结果:console.log 被触发,但这只是因为我们手动修改了该实例的 set 方法,与响应式监听无关。这种方式无法实现以下功能:
- 无法监听其他方法(如
map.get()
、map.delete()
)。 - 无法自动应用于所有 Map 实例(需为每个实例手动覆盖方法)。
- 无法处理通过原型链调用的方法(如
Map.prototype.set.call(map, ...)
)。
示例 2:使用 Proxy 监听 所有方法
const map = new Map();
// 使用 Proxy 拦截所有方法调用
const proxyMap = new Proxy(map, {
get(target, prop) {
if (typeof target[prop] === 'function') {
return function (...args) {
console.log(`拦截方法 ${prop},参数: ${args}`);
return target[prop].apply(target, args);
};
}
return target[prop];
}
});
proxyMap.set('key', 'value'); // 输出:拦截方法 set,参数: key,value
proxyMap.get('key'); // 输出:拦截方法 get,参数: key
所有方法调用均被拦截,且无需手动修改实例方法。
5. 性能问题
- 初始化开销:为对象的每个属性定义 getter/setter 需要遍历所有属性,对大型对象不友好。
- 内存占用:每个属性需维护独立的依赖收集逻辑,可能增加内存消耗。
const bigObject = {};
// 为对象的每个属性定义 getter/setter
for (let i = 0; i < 1000000; i++) {
Object.defineProperty(bigObject, `key${i}`, {
get() {
return this[`_key${i}`];
},
set(val) {
this[`_key${i}`] = val;
}
});
}
console.log('初始化完成'); // 初始化需要遍历所有属性,耗时长
Proxy 如何解决这些问题
const data = {
a: 1,
arr: [1, 2, 3],
nested: { value: 1 }
};
const proxy = new Proxy(data, {
get(target, key) {
console.log(`读取属性 ${key}`);
return Reflect.get(target, key);
},
set(target, key, value) {
console.log(`设置属性 ${key}`);
return Reflect.set(target, key, value);
},
deleteProperty(target, key) {
console.log(`删除属性 ${key}`);
return Reflect.deleteProperty(target, key);
}
});
proxy.a = 2; // 触发 setter
proxy.arr[3] = 4; // 触发 arr 的 getter(需递归代理)
proxy.newProp = 'test'; // 触发 setter(支持新增属性)
delete proxy.a; // 触发 deleteProperty
Proxy 可以按需劫持嵌套对象(惰性劫持),无需初始化时递归遍历,性能更优。
总结
Vue2 的实现 ( Object.defineProperty )
Object.defineProperty 支持 IE9 及以上版本,兼容性非常好。它会递归遍历对象,对每个属性单独设置 getter 和 setter ,但也存在以下局限性:
- 无法检测对象属性的添加或删除。 Vue2 在新增或删除对象属性时不会触发视图更新,需通过
Vue.set
或Vue.delete
手动处理。 - 数组监听受限。无法直接监听数组索引的修改(如
arr[0] = 1
)和 length 变化,因此 Vue2 重写了数组的一些方法来解决这个问题。 - 性能开销较大。需要递归地为每个属性设置 getter 和 setter ,对深层嵌套的对象和大型数组性能较差。
- 不支持 Map/Set 等数据结构。只能代理普通对象和数组,不能处理像 Map、Set 等复杂数据结构。
Vue3 的实现 ( Proxy ),更优的性能和更全面的响应式支持:
- 支持对象的动态属性的添加和删除。 Proxy 可以直接代理整个对象,因此可以监听属性的动态增加和删除,不再需要手动操作。
- 支持数组和索引修改。Proxy 能够监听数组索引的修改以及 length 变化,避免了重写数组方法。
- 性能更优。Proxy 采用惰性劫持,只有在访问属性时才会递归代理子对象,避免了递归遍历的性能开销。
- 支持更多数据结构。除了普通对象和数组,Proxy 还可以代理 Map、Set 等数据结构。
特性 | vue2(Object.defineProperty) | vue3(Proxy) |
---|---|---|
动态属性添加和删除 | 不支持(需要手动操作 Vue.set 或 Vue.delete ) | 支持 |
数组索引修改 | 需要重写方法(如 push、pop 等) | 支持 |
性能 | 递归初始化所有属性,性能较差 | 惰性劫持,按需触发,性能更优 |
Map/Set 等数据结构 | 不支持,仅支持普通对象或数组 | 支持 |
兼容性 | 支持 IE9+ | 不支持 IE |