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 原生支持泛型 |
常见面试问题
Q1: Vue 2 的 Object.defineProperty 有哪些局限性?
答案:
-
无法检测属性的添加和删除
// Vue 2 中这些操作不会触发更新
this.obj.newProp = 'value'; // ❌ 添加属性
delete this.obj.prop; // ❌ 删除属性
// 必须使用特殊 API
this.$set(this.obj, 'newProp', 'value');
this.$delete(this.obj, 'prop'); -
无法检测数组索引和长度变化
this.arr[0] = 'new'; // ❌ 不会触发更新
this.arr.length = 0; // ❌ 不会触发更新 -
初始化时需要递归遍历所有属性,性能开销大
-
需要重写数组方法(push、pop、splice 等)
Q2: Proxy 相比 Object.defineProperty 有什么优势?
答案:
| 特性 | Object.defineProperty | Proxy |
|---|---|---|
| 监听属性添加 | ❌ | ✅ |
| 监听属性删除 | ❌ | ✅ |
| 监听数组索引 | ❌ | ✅ |
| 监听数组长度 | ❌ | ✅ |
| 惰性代理 | ❌ 初始化递归 | ✅ 访问时代理 |
| 拦截操作数量 | 仅 get/set | 13 种操作 |
Q3: 什么是惰性代理?它有什么好处?
答案:
惰性代理(Lazy Proxy)指的是只有在访问嵌套对象时,才对其进行代理,而不是在初始化时递归代理所有属性。
// Vue 2:初始化时递归所有属性(即使从不访问)
const data = { a: { b: { c: 1 } } };
// 会立即递归处理 a、a.b、a.b.c
// Vue 3:只在访问时才代理
const proxy = reactive({ a: { b: { c: 1 } } });
// 只有访问 proxy.a 时才代理 a 对象
// 只有访问 proxy.a.b 时才代理 b 对象
好处:
- 初始化更快:不需要遍历所有属性
- 内存占用更低:未访问的属性不会被代理
- 按需代理:对大型数据结构尤其有效
Q4: Proxy 为什么无法被 polyfill?Vue 3 如何处理兼容性?
答案:
无法 polyfill 的原因:
- Proxy 是 ES6 的元编程特性,它在语言层面拦截对象操作
- Object.defineProperty 无法模拟 Proxy 的所有 trap(如
has、ownKeys) - 无法通过 JavaScript 代码实现相同的底层机制
Vue 3 的兼容性处理:
// Vue 3 不支持 IE11,这是有意的设计决策
// 如果需要兼容 IE11,有以下方案:
// 1. 继续使用 Vue 2
// 2. 使用 @vue/compat 兼容包(功能受限)
// 3. 对 IE 用户提供降级方案
Q5: Reflect 在 Vue 3 响应式中起什么作用?
答案:
const handler: ProxyHandler<object> = {
get(target, key, receiver) {
// ✅ 使用 Reflect,正确处理 getter 中的 this
return Reflect.get(target, key, receiver);
// ❌ 直接访问,可能导致 this 指向问题
// return target[key];
},
set(target, key, value, receiver) {
// Reflect.set 返回布尔值,表示是否成功
return Reflect.set(target, key, value, receiver);
}
};
Reflect 的作用:
- 保持正确的 this 绑定:receiver 参数确保 getter/setter 中的 this 指向代理对象
- 返回操作结果:Reflect 方法返回布尔值表示操作是否成功
- 与 Proxy trap 一一对应:每个 Proxy trap 都有对应的 Reflect 方法
Q6: 手写一个简化版的 reactive 函数
答案:
const reactiveMap = new WeakMap<object, object>();
function reactive<T extends object>(target: T): T {
// 避免重复代理
if (reactiveMap.has(target)) {
return reactiveMap.get(target) as T;
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
// 依赖收集(简化)
track(target, key);
// 惰性代理:嵌套对象在访问时才代理
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key as keyof T];
const result = Reflect.set(target, key, value, receiver);
// 触发更新
if (oldValue !== value) {
trigger(target, key);
}
return result;
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
trigger(target, key);
return result;
}
});
reactiveMap.set(target, proxy);
return proxy;
}
// 简化的依赖收集和触发
function track(target: object, key: string | symbol) {
console.log(`收集依赖: ${String(key)}`);
}
function trigger(target: object, key: string | symbol) {
console.log(`触发更新: ${String(key)}`);
}