Vue 3的响应式系统是其核心特性之一,相比Vue 2的Object.defineProperty
实现,Vue 3采用ES6的Proxy
API重构了响应式机制,带来了更强大的功能和更好的性能。本文将通过实战代码,从底层原理到实际应用,解析Proxy如何实现数据响应式,以及Vue 3响应式系统的核心工作流程。
一、从Object.defineProperty到Proxy的进化
Vue 2的响应式实现依赖Object.defineProperty
,通过遍历对象属性并为其设置getter和setter来追踪变化。但这种方式存在固有的局限性:
- 无法监听对象新增属性或删除属性
- 无法监听数组索引和长度变化
- 需要递归遍历对象,性能开销大
Vue 3改用Proxy
解决了这些问题。Proxy
可以创建一个对象的代理,拦截并自定义对象的基本操作(如属性访问、赋值、删除等),具有以下优势:
- 原生支持监听对象新增/删除属性
- 天然支持数组变化监听
- 非侵入式拦截,不需要修改原对象
- 可以拦截更多操作(如
in
、delete
、函数调用
等)
二、Proxy基础:拦截器实战
Proxy
的基本用法是创建一个代理对象,通过拦截器(handler)定义拦截行为。以下是响应式系统中常用的拦截器:
// 原始对象
const target = {name: 'Vue',version: 3,features: ['reactive', 'composition-api']
};// 创建代理对象
const handler = {// 拦截属性访问:obj.prop 或 obj[prop]get(target, prop, receiver) {console.log(`访问属性: ${prop}`);return Reflect.get(target, prop, receiver);},// 拦截属性赋值:obj.prop = valueset(target, prop, value, receiver) {console.log(`设置属性: ${prop} = ${value}`);return Reflect.set(target, prop, value, receiver);},// 拦截属性删除:delete obj.propdeleteProperty(target, prop) {console.log(`删除属性: ${prop}`);return Reflect.deleteProperty(target, prop);},// 拦截in操作符:prop in objhas(target, prop) {console.log(`检查属性是否存在: ${prop}`);return Reflect.has(target, prop);}
};const proxy = new Proxy(target, handler);// 测试拦截器
proxy.name; // 访问属性: name → 返回 'Vue'
proxy.version = 3.3; // 设置属性: version = 3.3 → 返回 true
'features' in proxy; // 检查属性是否存在: features → 返回 true
delete proxy.version; // 删除属性: version → 返回 true
Reflect
是ES6引入的内置对象,提供了方法,确保拦截操作的默认行为可以正确执行,同时返回规范的结果。
三、实现简易响应式系统
Vue 3的响应式系统核心是"依赖收集-触发更新"机制:当访问响应式数据时收集依赖(副作用函数),当数据变化时触发这些依赖重新执行。
1. 依赖收集容器
首先需要一个容器存储依赖,以及一个方法追踪当前活跃的副作用函数:
// 存储依赖的容器:key为目标对象,value为该对象的依赖映射
const targetMap = new WeakMap();// 当前活跃的副作用函数
let activeEffect = null;// 追踪副作用函数
function track(target, prop) {if (!activeEffect) return;// 获取目标对象的依赖映射let depsMap = targetMap.get(target);if (!depsMap) {targetMap.set(target, (depsMap = new Map()));}// 获取属性的依赖集合let dep = depsMap.get(prop);if (!dep) {depsMap.set(prop, (dep = new Set()));}// 将当前副作用函数添加到依赖集合dep.add(activeEffect);
}// 触发依赖更新
function trigger(target, prop) {const depsMap = targetMap.get(target);if (!depsMap) return;const dep = depsMap.get(prop);if (dep) {// 执行所有依赖的副作用函数dep.forEach(effect => effect());}
}
使用WeakMap
存储目标对象的依赖,避免内存泄漏;使用Set
存储副作用函数,自动去重。
2. 创建响应式对象
结合Proxy和依赖收集,实现响应式对象创建函数:
// 创建响应式对象
function reactive(target) {// 仅对对象和数组进行代理if (typeof target !== 'object' || target === null) {return target;}// 创建代理对象const handler = {get(target, prop, receiver) {const result = Reflect.get(target, prop, receiver);// 收集依赖track(target, prop);// 递归处理嵌套对象return reactive(result);},set(target, prop, value, receiver) {const oldValue = Reflect.get(target, prop, receiver);// 如果值未变化,不触发更新if (oldValue === value) return true;const result = Reflect.set(target, prop, value, receiver);// 触发更新trigger(target, prop);return result;},deleteProperty(target, prop) {const result = Reflect.deleteProperty(target, prop);// 触发更新trigger(target, prop);return result;}};return new Proxy(target, handler);
}
这里的关键是:
- 递归处理嵌套对象,确保深层属性也具有响应性
- 在
get
中收集依赖,在set
和deleteProperty
中触发更新 - 避免值未变化时的无效更新
3. 副作用函数注册
实现effect
函数,用于注册依赖响应式数据的副作用函数:
// 注册副作用函数
function effect(fn) {// 保存当前副作用函数activeEffect = fn;// 执行函数,触发依赖收集fn();// 重置当前副作用函数activeEffect = null;
}
四、实战测试:响应式系统工作流程
通过一个完整示例测试上述实现:
// 创建响应式对象
const state = reactive({count: 0,user: {name: 'Alice'},hobbies: ['reading']
});// 注册副作用函数1:监听count变化
effect(() => {console.log(`count变化: ${state.count}`);
});// 注册副作用函数2:监听user.name变化
effect(() => {console.log(`用户名变化: ${state.user.name}`);
});// 测试基本操作
state.count++; // 触发副作用1 → 输出 "count变化: 1"
state.user.name = 'Bob'; // 触发副作用2 → 输出 "用户名变化: Bob"// 测试新增属性
state.age = 20; // 新增属性,不触发任何副作用
effect(() => {console.log(`年龄变化: ${state.age}`); // 注册新副作用
});
state.age = 21; // 触发新增的副作用 → 输出 "年龄变化: 21"// 测试删除属性
delete state.age; // 触发副作用 → 输出 "年龄变化: undefined"// 测试数组操作
effect(() => {console.log(`爱好列表: ${state.hobbies.join(',')}`);
});
state.hobbies.push('coding'); // 触发数组副作用 → 输出 "爱好列表: reading,coding"
state.hobbies[0] = 'writing'; // 触发数组副作用 → 输出 "爱好列表: writing,coding"
这个示例完整展示了响应式系统的工作流程:
reactive
将普通对象转换为响应式代理effect
注册副作用函数,执行时触发get
拦截器get
拦截器调用track
收集依赖- 数据变化时触发
set
或deleteProperty
拦截器 - 拦截器调用
trigger
执行所有相关的副作用函数
五、Vue 3响应式的高级特性
实际的Vue 3响应式系统还包含更多优化和特性:
1. 只读代理
使用readonly
创建只读响应式对象,禁止修改操作:
function readonly(target) {return new Proxy(target, {get(target, prop, receiver) {const result = Reflect.get(target, prop, receiver);return readonly(result); // 递归处理},set(target, prop, value, receiver) {console.warn(`Cannot set property ${prop} of readonly object`);return false;},deleteProperty(target, prop) {console.warn(`Cannot delete property ${prop} of readonly object`);return false;}});
}// 使用示例
const info = readonly({ name: 'Vue 3' });
info.name = 'Vue 4'; // 警告:Cannot set property name of readonly object
2. 浅层响应式
shallowReactive
只处理对象本身的响应性,不递归处理嵌套对象:
function shallowReactive(target) {return new Proxy(target, {get(target, prop, receiver) {track(target, prop);return Reflect.get(target, prop, receiver); // 不递归},set(target, prop, value, receiver) {const result = Reflect.set(target, prop, value, receiver);trigger(target, prop);return result;}});
}
适用于已知嵌套对象不会变化的场景,提升性能。
3. 响应式判断工具
Vue 3提供了工具函数判断对象是否为响应式:
// 标记响应式对象
const reactiveFlag = Symbol('isReactive');function reactive(target) {// ...const handler = {get(target, prop, receiver) {if (prop === reactiveFlag) return true;// ...}// ...};
}// 判断是否为响应式对象
function isReactive(value) {return value && value[reactiveFlag] === true;
}
六、总结
Vue 3基于Proxy的响应式系统通过拦截对象操作,实现了更全面、更高效的响应式能力。核心流程是:
- 使用
reactive
创建代理对象,通过Proxy拦截属性访问和修改 - 访问属性时,
track
函数收集依赖的副作用函数 - 修改属性时,
trigger
函数触发所有相关的副作用函数执行
相比Vue 2的实现,Proxy方案解决了对象新增/删除属性监听、数组变化监听等问题,同时通过懒递归(访问时才处理嵌套对象)提升了性能。