Vue3响应式原理:Proxy实现机制
一、Proxy的核心优势
Vue3选择Proxy作为响应式基础,相比Vue2的Object.defineProperty具有三大突破性改进:
全面拦截能力
Proxy支持13种拦截操作(如get/set/deleteProperty/has等),可捕获对象增删改查等所有操作。而Vue2只能通过getter/setter拦截已有属性,无法检测新增/删除属性(需借助Vue.set/Vue.delete)。按需代理机制
Proxy仅在属性被访问时触发代理逻辑,避免Vue2初始化时递归遍历所有属性的性能损耗。例如嵌套对象{a: {b: 1}},只有访问a.b时才会生成子代理。原生数组支持
Proxy直接拦截数组的push/pop等方法,无需像Vue2那样重写数组原型方法。
二、响应式核心架构
Vue3的响应式系统由三大模块构成:
graph TD
A[响应式对象] --> B(Proxy拦截)
B --> C[依赖收集]
B --> D[触发更新]
C --> E[WeakMap存储依赖关系]
D --> F[执行副作用函数]
1. 代理对象创建
通过reactive()函数创建响应式对象时,内部调用createReactiveObject()生成Proxy实例:
// 源码节选(简化)
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key); // 依赖收集
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 触发更新
}
return result;
}
});
}
Proxy的handler对象定义了get/set等拦截器,结合Reflect实现元编程。
2. 依赖收集机制
当读取属性时,通过track()建立三级依赖存储结构:
// 依赖关系数据结构
type Target = Object; // 原始对象
type Key = string; // 属性名
type Dep = Set<ReactiveEffect>; // 副作用集合
const targetMap = new WeakMap<Target, Map<Key, Dep>>();
- WeakMap:以原始对象为键,避免内存泄漏
- Map:存储对象属性与依赖的映射
- Set:存储同一属性的所有副作用函数
3. 副作用函数管理
通过effect()注册副作用函数:
let activeEffect: ReactiveEffect | null;
function effect(fn) {
const effectFn = () => {
cleanup(effectFn); // 清理旧依赖
activeEffect = effectFn;
fn();
activeEffect = null;
};
effectFn.deps = [];
effectFn();
}
- 全局变量
activeEffect标记当前运行的副作用 - 每次执行前清理旧依赖,避免无效更新
三、响应式流程全解析
通过示例代码演示完整响应链:
const state = reactive({ count: 0 });
effect(() => {
console.log('Count changed:', state.count);
});
state.count++; // 触发日志输出
初始化阶段
reactive()创建Proxy代理对象,此时未触发任何拦截操作。依赖收集阶段
effect()执行副作用函数,读取state.count触发Proxy的get拦截:- 调用
track(target, 'count'),将当前副作用存入targetMap
- 调用
更新触发阶段
修改state.count触发Proxy的set拦截:- 通过
trigger(target, 'count')从targetMap中取出依赖集合 - 调度执行所有关联的副作用函数
- 通过
四、进阶特性实现
1. 嵌套对象处理
Proxy自动递归代理嵌套对象:
const obj = reactive({
nested: { value: 1 }
});
// 访问nested时自动生成子代理
console.log(obj.nested.value); // 触发get拦截
2. 数组特殊处理
通过重写数组方法实现深度响应:
// 源码中的数组拦截器
const arrayInstrumentations = {
includes(...args) {
return arrMethods.includes.apply(toRaw(this), args);
},
// 其他方法重写...
}
3. 性能优化策略
- 懒代理:仅在属性被访问时生成子代理
- 缓存机制:通过
proxyMap缓存已代理对象 - 批量更新:通过微任务队列合并触发操作
五、与Vue2实现对比
| 特性 | Vue3 (Proxy) | Vue2 (defineProperty) |
|---|---|---|
| 拦截范围 | 全属性(包括新增/删除) | 仅限初始化时存在的属性 |
| 数组支持 | 原生方法直接拦截 | 需重写数组原型方法 |
| 性能表现 | 按需代理,内存占用更低 | 递归遍历初始化,内存开销大 |
| 嵌套对象处理 | 自动延迟代理 | 需要深度遍历初始化 |
| 代码复杂度 | 核心代码约300行(更简洁) | 核心代码约800行(包含各种补丁) |
六、实战注意事项
避免解构丢失响应性
使用toRefs()保持解构后的响应性:const state = reactive({ count: 0 }); const { count } = toRefs(state); // 保持响应谨慎使用深度监听
watch(obj, callback, { deep: true })可能引发性能问题,建议按需监听。Ref与Reactive选择
- 基本类型用
ref - 对象/数组用
reactive - 需要传递引用时用
ref包裹对象
- 基本类型用