Vue 3 为什么使用 Proxy
问题
Vue 3.0 中为什么要使用 Proxy?它相比以前的实现方式有什么改进?
答案
Vue 3 使用 Proxy 替代了 Vue 2 中的 Object.defineProperty 来实现响应式系统,这是一个重大的架构改进。
Vue 2 的实现方式(Object.defineProperty)
function defineReactive(obj: Record<string, any>, key: string, val: any) {
Object.defineProperty(obj, key, {
get() {
console.log(`读取 ${key}: ${val}`);
return val;
},
set(newVal) {
console.log(`设置 ${key}: ${newVal}`);
val = newVal;
}
});
}
const data = { name: 'Vue' };
defineReactive(data, 'name', data.name);
Object.defineProperty 的局限性
- 无法检测对象属性的添加和删除 - 需要使用
Vue.set()/Vue.delete() - 无法检测数组索引的变化 -
arr[0] = newValue不会触发更新 - 无法检测数组长度的修改 -
arr.length = 0不会触发更新 - 需要递归遍历 - 初始化时需要遍历所有属性,性能开销大
- 需要单独处理数组 - Vue 2 重写了数组的 7 个方法
Vue 3 的实现方式(Proxy)
function reactive<T extends object>(target: T): T {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
console.log(`读取 ${String(key)}: ${result}`);
// 如果是对象,递归代理(惰性代理)
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
console.log(`设置 ${String(key)}: ${value}`);
return Reflect.set(target, key, value, receiver);
},
deleteProperty(target, key) {
console.log(`删除 ${String(key)}`);
return Reflect.deleteProperty(target, key);
}
});
}
const data = reactive({ name: 'Vue', list: [1, 2, 3] });
// 以下操作都能被检测到
data.name = 'Vue3'; // ✅ 修改属性
data.version = '3.0'; // ✅ 添加新属性
delete data.name; // ✅ 删除属性
data.list[0] = 100; // ✅ 修改数组索引
data.list.push(4); // ✅ 数组方法
Proxy 的优势
| 特性 | Object.defineProperty | Proxy |
|---|---|---|
| 检测属性添加 | ❌ 需要 Vue.set | ✅ 原生支持 |
| 检测属性删除 | ❌ 需要 Vue.delete | ✅ 原生支持 |
| 检测数组索引变化 | ❌ 不支持 | ✅ 原生支持 |
| 检测数组长度变化 | ❌ 不支持 | ✅ 原生支持 |
| 惰性代理 | ❌ 初始化递归 | ✅ 访问时代理 |
| 代理对象本身 | ❌ 代理属性 | ✅ 代理整个对象 |
| 性能 | 初始化开销大 | 初始化快,运行时略慢 |
核心改进详解
1. 惰性代理(Lazy Proxy)
Vue 2 在初始化时递归遍历所有属性,而 Vue 3 只在访问时才代理嵌套对象:
// Vue 2:初始化时递归所有属性
function observe(obj: object) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
if (typeof obj[key] === 'object') {
observe(obj[key]); // 递归
}
});
}
// Vue 3:访问时才代理
const handler: ProxyHandler<object> = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
if (typeof result === 'object' && result !== null) {
return new Proxy(result, handler); // 惰性代理
}
return result;
}
};
性能提升
对于大型对象,Vue 3 的惰性代理可以显著减少初始化时间,因为只有实际访问的属性才会被代理。
2. 完整的拦截能力
Proxy 提供了 13 种拦截操作:
const handler: ProxyHandler<object> = {
get(target, prop, receiver) {}, // 读取属性
set(target, prop, value, receiver) {}, // 设置属性
deleteProperty(target, prop) {}, // 删除属性
has(target, prop) {}, // in 操作符
ownKeys(target) {}, // Object.keys() 等
getOwnPropertyDescriptor(target, prop) {},
defineProperty(target, prop, descriptor) {},
getPrototypeOf(target) {},
setPrototypeOf(target, proto) {},
isExtensible(target) {},
preventExtensions(target) {},
apply(target, thisArg, args) {}, // 函数调用
construct(target, args, newTarget) {} // new 操作符
};
3. 更好的数组支持
const arr = reactive([1, 2, 3]);
// 以下操作在 Vue 3 中都能正确触发更新
arr[0] = 100; // ✅ 索引赋值
arr[10] = 'new'; // ✅ 稀疏数组
arr.length = 1; // ✅ 修改长度
arr.push(4); // ✅ 数组方法
兼容性考虑
浏览器支持
Proxy 是 ES6 特性,无法被 polyfill,因此 Vue 3 不支持 IE11。
如果需要支持 IE11,可以考虑:
- 继续使用 Vue 2
- 使用 Vue 3 的
@vue/compat兼容构建
总结
| 改进点 | 说明 |
|---|---|
| 更全面的响应式 | 可检测属性的添加、删除,数组索引和长度变化 |
| 更好的性能 | 惰性代理,减少初始化开销 |
| 更简洁的代码 | 不需要 Vue.set/Vue.delete,不需要重写数组方法 |
| 更好的 TypeScript 支持 | Proxy 原生支持泛型 |