Vue3.0 为什么要用 Proxy API 替代 defineProperty API?
问题解析
这是 Vue3 最核心的底层变化之一。面试考察这个问题,是想要了解候选人对响应式原理的深入理解,以及能否从技术原理层面分析两种方案的优劣。
核心概念
两种响应式方案对比
┌─────────────────────────────────────────────────────┐
│ 响应式方案对比 │
├───────────────────────────────────┬─────────────────┤
│ Vue2 defineProperty │ Vue3 Proxy │
├───────────────────────────────────┼─────────────────┤
│ 遍历对象属性逐一劫持 │ 直接代理整个对象 │
│ 无法监听新增/删除属性 │ 自动监听新增/删除 │
│ 无法监听数组索引变化 │ 自动监听数组索引 │
│ 需要重写数组方法 │ 原生支持数组方法 │
│ 嵌套对象需要深度递归 │ 懒递归,访问时才代理 │
│ 兼容 IE9+ │ 不兼容 IE,无 polyfill │
└───────────────────────────────────┴─────────────────┘
详细解答
一、Object.defineProperty 详解
1. 基本实现原理
function defineReactive(obj, key, val) {
// 递归处理嵌套对象
observe(val);
const dep = new Dep(); // 依赖收集器
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 依赖收集
if (Dep.target) {
dep.depend();
}
console.log(`获取 ${key}: ${val}`);
return val;
},
set(newVal) {
if (newVal === val) return;
console.log(`设置 ${key}: ${newVal}`);
val = newVal;
// 递归处理新值
observe(newVal);
// 触发更新
dep.notify();
}
});
}
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return;
}
// 遍历所有属性进行劫持
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
// 使用
const data = { name: 'Tom', age: 20 };
observe(data);
data.name; // 获取 name: Tom
data.name = 'Jerry'; // 设置 name: Jerry
data.age++; // 获取 age: 20, 设置 age: 21
2. 存在的问题
问题1:无法监听新增属性
const data = { name: 'Tom' };
observe(data);
// 新增属性
data.age = 20; // ❌ 无法监听,不会触发更新
// Vue2 解决方案
Vue.set(data, 'age', 20); // 需要显式调用
问题2:无法监听删除属性
const data = { name: 'Tom', age: 20 };
observe(data);
// 删除属性
delete data.age; // ❌ 无法监听,不会触发更新
// Vue2 解决方案
Vue.delete(data, 'age'); // 需要显式调用
问题3:数组问题
const arr = [1, 2, 3];
observe(arr);
// 通过索引修改
arr[0] = 100; // ✅ 可以监听(如果已存在索引)
// 通过索引新增
arr[3] = 4; // ❌ 无法监听
// 修改数组长度
arr.length = 10; // ❌ 无法监听
// 数组方法
arr.push(5); // ❌ 无法监听(需要重写方法)
arr.pop(); // ❌ 无法监听
Vue2 的数组解决方案 - 重写数组方法:
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
const original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
// 插入的元素也需要被监听
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted);
// 通知更新
ob.dep.notify();
return result;
},
enumerable: false,
writable: true,
configurable: true
});
});
// 替换数组原型
function observeArray(items) {
for (let i = 0; i < items.length; i++) {
observe(items[i]);
}
}
问题4:深度递归性能问题
const data = {
a: {
b: {
c: {
d: 1 // 嵌套层级深
}
}
}
};
// Vue2: 初始化时需要深度递归所有属性
observe(data);
// 需要递归: a -> b -> c -> d
// 即使某些属性永远不会被访问,也会被监听
二、Proxy 详解
1. 基本实现原理
function reactive(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
console.log(`获取 ${key}: ${res}`);
// 依赖收集
track(target, key);
// 懒递归:访问时才建立响应式
return isObject(res) ? reactive(res) : res;
},
set(target, key, value, receiver) {
const hadKey = hasOwn(target, key);
const oldValue = target[key];
const res = Reflect.set(target, key, value, receiver);
if (!hadKey) {
// 新增属性
console.log(`新增 ${key}: ${value}`);
trigger(target, key, 'add');
} else if (hasChanged(value, oldValue)) {
// 修改属性
console.log(`修改 ${key}: ${value}`);
trigger(target, key, 'set');
}
return res;
},
deleteProperty(target, key) {
const hadKey = hasOwn(target, key);
const res = Reflect.deleteProperty(target, key);
if (hadKey) {
// 删除属性
console.log(`删除 ${key}`);
trigger(target, key, 'delete');
}
return res;
},
// 更多拦截器
has(target, key) {
// in 操作符
const res = Reflect.has(target, key);
track(target, key);
return res;
},
ownKeys(target) {
// Object.keys, for...in
track(target, ITERATE_KEY);
return Reflect.ownKeys(target);
}
});
return observed;
}
2. 解决 defineProperty 的所有问题
解决新增/删除属性问题:
const state = reactive({ name: 'Tom' });
// 新增属性
state.age = 20; // ✅ 自动监听,触发 'add' 类型更新
// 删除属性
delete state.name; // ✅ 自动监听,触发 'delete' 类型更新
解决数组问题:
const arr = reactive([1, 2, 3]);
// 通过索引修改
arr[0] = 100; // ✅ 自动监听
// 通过索引新增
arr[3] = 4; // ✅ 自动监听
// 修改数组长度
arr.length = 10; // ✅ 自动监听
// 数组方法
arr.push(5); // ✅ 原生支持
arr.pop(); // ✅ 原生支持
arr.splice(1, 1); // ✅ 原生支持
解决性能问题 - 懒递归:
const data = reactive({
a: {
b: {
c: {
d: 1
}
}
}
});
// 初始化时:只代理最外层对象,不会递归
// 访问 data.a 时:才对 a 建立响应式
// 访问 data.a.b 时:才对 b 建立响应式
// 以此类推...
console.log(data.a.b.c.d); // 逐层建立响应式
3. Proxy 的其他优势
支持更多拦截器:
const proxy = new Proxy(obj, {
get(target, key, receiver) {}, // 读取属性
set(target, key, value, receiver) {}, // 设置属性
deleteProperty(target, key) {}, // 删除属性
has(target, key) {}, // in 操作符
ownKeys(target) {}, // Object.keys, for...in
getOwnPropertyDescriptor(target, key) {},
defineProperty(target, key, descriptor) {},
preventExtensions(target) {}, // Object.preventExtensions
getPrototypeOf(target) {}, // Object.getPrototypeOf
setPrototypeOf(target, proto) {}, // Object.setPrototypeOf
apply(target, thisArg, args) {}, // 函数调用
construct(target, args, newTarget) {} // new 操作符
});
支持 Map/Set/WeakMap/WeakSet:
const map = reactive(new Map());
map.set('key', 'value'); // ✅ 可以监听
map.get('key'); // ✅ 可以监听
map.has('key'); // ✅ 可以监听
map.delete('key'); // ✅ 可以监听
map.clear(); // ✅ 可以监听
const set = reactive(new Set());
set.add(1); // ✅ 可以监听
set.has(1); // ✅ 可以监听
set.delete(1); // ✅ 可以监听
三、完整对比
| 特性 | defineProperty | Proxy |
|---|---|---|
| 拦截目标 | 单个属性 | 整个对象 |
| 新增属性 | ❌ 不支持 | ✅ 支持 |
| 删除属性 | ❌ 不支持 | ✅ 支持 |
| 数组索引 | ❌ 不支持 | ✅ 支持 |
| 数组长度 | ❌ 不支持 | ✅ 支持 |
| Map/Set | ❌ 不支持 | ✅ 支持 |
| 初始化方式 | 深度递归 | 懒递归 |
| IE 兼容 | IE9+ | ❌ 不支持 |
深入理解
1. 依赖收集的优化
Vue2 的依赖收集:
// 每个属性有自己的 Dep
data: {
name: 'Tom', // Dep1
age: 20, // Dep2
info: { // Dep3
address: 'Beijing' // Dep4
}
}
// 4 个 Dep 实例
Vue3 的依赖收集:
// 使用 WeakMap 存储依赖关系
const targetMap = new WeakMap();
// 结构:
// WeakMap {
// target(obj) -> Map {
// key -> Set [effect1, effect2]
// }
// }
track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, depsMap = new Map());
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, dep = new Set());
}
dep.add(activeEffect);
}
2. Vue3 响应式系统的分层设计
┌─────────────────────────────────────────┐
│ 应用层 (Vue Component) │
├─────────────────────────────────────────┤
│ 运行时核心 (runtime-core) │
│ • 组件实例管理 │
│ • 生命周期 │
│ • 调度器 (Scheduler) │
├─────────────────────────────────────────┤
│ 响应式系统 (reactivity) │
│ • ref / reactive / readonly │
│ • computed / watch / watchEffect │
│ • effect 副作用管理 │
├─────────────────────────────────────────┤
│ 底层实现 │
│ • Proxy 代理 │
│ • Reflect 反射 │
│ • WeakMap 依赖存储 │
└─────────────────────────────────────────┘
3. ref vs reactive 的实现差异
// ref - 对基本类型的包装
function ref(value) {
return createRef(value, false);
}
function createRef(rawValue, shallow) {
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}
class RefImpl {
constructor(value, __v_isShallow) {
this.__v_isShallow = __v_isShallow;
this._value = __v_isShallow ? value : toReactive(value);
this._rawValue = value;
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = this.__v_isShallow ? newVal : toReactive(newVal);
triggerRefValue(this);
}
}
}
// reactive - 对象的响应式代理
function reactive(target) {
return createReactiveObject(
target,
false,
mutableHandlers, // Proxy handlers
mutableCollectionHandlers
);
}
最佳实践
1. 选择合适的响应式 API
import { ref, reactive, shallowRef, shallowReactive, readonly } from 'vue';
// 基本类型 - 使用 ref
const count = ref(0);
const name = ref('Tom');
const isActive = ref(false);
// 对象类型 - 使用 reactive
const state = reactive({
user: { name: 'Tom', age: 20 },
list: []
});
// 大型数据 - 使用 shallowRef/shallowReactive
const bigList = shallowRef([
{ id: 1, /* ... */ },
{ id: 2, /* ... */ },
// 10000 条数据
]);
// 只读数据 - 使用 readonly
const config = readonly({
apiUrl: 'https://api.example.com',
timeout: 5000
});
2. 避免解构丢失响应性
const state = reactive({ count: 0, name: 'Tom' });
// ❌ 错误:解构会丢失响应性
const { count, name } = state;
count++; // 不会触发更新
// ✅ 正确:使用 toRefs
import { toRefs } from 'vue';
const { count, name } = toRefs(state);
count.value++; // 会触发更新
// ✅ 正确:在模板中直接使用
// {{ state.count }} 会保持响应性
3. 数组和集合的正确使用
const state = reactive({
list: []
});
// ✅ 正确:使用数组方法
state.list.push(item);
state.list.splice(index, 1);
state.list.sort((a, b) => a - b);
// ✅ 正确:直接替换整个数组
state.list = newList;
// ✅ 正确:使用响应式 Map/Set
const map = reactive(new Map());
map.set('key', value);
const set = reactive(new Set());
set.add(value);
面试要点
-
能够清晰解释 defineProperty 的缺陷
- 无法监听新增/删除属性
- 无法监听数组索引和长度变化
- 初始化时需要深度递归
-
理解 Proxy 的优势
- 直接代理整个对象
- 自动监听新增/删除属性
- 原生支持数组操作
- 懒递归,性能更好
-
了解 Vue2 的解决方案
- Vue.set / Vue.delete
- 重写数组方法
- 这些方案增加了心智负担
-
理解懒递归的优化
- 访问时才建立深层响应式
- 减少了初始化开销
-
知道 Proxy 的兼容性限制
- 不兼容 IE11
- 无法 polyfill
核心结论:
- Proxy 解决了 defineProperty 的所有已知问题
- Proxy 提供了更强大和灵活的拦截能力
- Vue3 的响应式系统更加完善和高效
- 代价是不再支持 IE,需要权衡