Vue的响应式系统是其核心特性之一,从Vue2到Vue3,响应式的实现方案从Object.defineProperty演进为Proxy。相比前者,Proxy能原生支持数组、对象新增属性等场景,且对对象的拦截更全面。本文将从核心原理出发,手把手教你实现一个基于Proxy + effect的简易Vue响应式系统,帮你彻底搞懂响应式的底层逻辑。
一、响应式的核心原理是什么?
响应式的本质是"数据变化驱动视图更新",其核心逻辑可拆解为三个关键步骤:
- 依赖收集:当组件渲染(或effect执行)时,会访问响应式数据,此时记录"数据-依赖(effect)"的映射关系;
- 数据拦截:通过Proxy拦截响应式数据的读取(get)和修改(set)操作------读取时触发依赖收集,修改时触发依赖更新;
- 依赖触发:当响应式数据被修改时,找到之前收集的所有依赖(effect),并重新执行这些依赖,从而实现视图更新或其他副作用触发。
其中,Proxy负责"数据拦截",effect负责封装"依赖(副作用函数)",再配合一个"依赖映射表"完成整个响应式闭环。
二、核心模块拆解与实现
我们将分三步实现简易响应式系统:先实现effect模块封装副作用,再实现reactive模块基于Proxy拦截数据,最后通过依赖映射表关联两者,完成依赖收集与触发。
1. 第一步:实现effect------副作用函数封装
effect的作用是包裹需要响应式触发的副作用函数(比如组件渲染函数、watch回调等)。当effect执行时,会主动触发响应式数据的get操作,进而触发依赖收集;当数据变化时,effect会被重新执行。
核心逻辑:
- 定义一个全局变量(activeEffect),用于标记当前正在执行的effect;
- effect函数接收一个副作用函数(fn),执行fn前将其赋值给activeEffect,执行后清空activeEffect(避免非响应式数据访问时误收集依赖)。
代码实现:
javascript
// 全局变量:标记当前活跃的effect(正在执行的副作用函数)
let activeEffect = null;
/**
* 副作用函数封装
* @param {Function} fn - 需要响应式触发的副作用函数
*/
function effect(fn) {
// 定义一个包装函数,便于后续扩展(如错误处理、调度执行等)
const effectFn = () => {
// 执行副作用函数前,先标记当前活跃的effect
activeEffect = effectFn;
// 执行副作用函数(此时会访问响应式数据,触发get拦截,进而收集依赖)
fn();
// 执行完成后,清空标记(避免后续非响应式数据访问时误收集)
activeEffect = null;
};
// 立即执行一次副作用函数,触发初始的依赖收集
effectFn();
}
2. 第二步:实现依赖映射表------track与trigger
我们需要一个数据结构来存储"数据-属性-effect"的映射关系,这里采用WeakMap(数据)→ Map(属性)→ Set(effect)的结构:
- WeakMap:key为响应式对象(target),value为Map(属性映射表),弱引用特性可避免内存泄漏;
- Map:key为对象的属性名(key),value为Set(存储该属性对应的所有effect);
- Set:存储effect,保证effect不重复(避免多次执行同一副作用)。
基于这个结构,实现两个核心函数:
- track:在响应式数据被读取时调用,收集依赖(将activeEffect存入映射表);
- trigger:在响应式数据被修改时调用,触发依赖(从映射表中取出effect并执行)。
代码实现:
javascript
// 依赖映射表:WeakMap(target) → Map(key) → Set(effect)
const targetMap = new WeakMap();
/**
* 收集依赖(响应式数据读取时触发)
* @param {Object} target - 响应式对象
* @param {string} key - 被读取的属性名
*/
function track(target, key) {
// 1. 若当前无活跃的effect,无需收集依赖,直接返回
if (!activeEffect) return;
// 2. 从targetMap中获取当前对象的属性映射表(Map)
let depsMap = targetMap.get(target);
if (!depsMap) {
// 若不存在,创建新的Map并存入targetMap
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 3. 从depsMap中获取当前属性的effect集合(Set)
let deps = depsMap.get(key);
if (!deps) {
// 若不存在,创建新的Set并存入depsMap
deps = new Set();
depsMap.set(key, deps);
}
// 4. 将当前活跃的effect存入Set(保证不重复)
deps.add(activeEffect);
}
/**
* 触发依赖(响应式数据修改时触发)
* @param {Object} target - 响应式对象
* @param {string} key - 被修改的属性名
*/
function trigger(target, key) {
// 1. 从targetMap中获取当前对象的属性映射表
const depsMap = targetMap.get(target);
if (!depsMap) return; // 若没有收集过依赖,直接返回
// 2. 从depsMap中获取当前属性的effect集合
const deps = depsMap.get(key);
if (deps) {
// 3. 遍历effect集合,执行每个effect(触发副作用更新)
deps.forEach(effect => effect());
}
}
3. 第三步:实现reactive------基于Proxy的响应式数据拦截
reactive函数的作用是将普通对象转为响应式对象,核心是通过Proxy拦截对象的get(读取)和set(修改)操作:
- get拦截:当读取响应式对象的属性时,调用track函数收集依赖;
- set拦截:当修改响应式对象的属性时,先更新属性值,再调用trigger函数触发依赖。
代码实现:
javascript
/**
* 将普通对象转为响应式对象(基于Proxy)
* @param {Object} target - 普通对象
* @returns {Proxy} 响应式对象
*/
function reactive(target) {
return new Proxy(target, {
// 拦截属性读取操作
get(target, key) {
// 1. 读取原始属性值
const value = Reflect.get(target, key);
// 2. 收集依赖(关联target、key和当前activeEffect)
track(target, key);
// 3. 返回属性值(若value是对象,可递归转为响应式,这里简化实现)
return value;
},
// 拦截属性修改操作
set(target, key, value) {
// 1. 修改原始属性值
const result = Reflect.set(target, key, value);
// 2. 触发依赖(执行该属性关联的所有effect)
trigger(target, key);
// 3. 返回修改结果(符合Proxy规范)
return result;
}
});
}
这里使用Reflect而非直接操作target,是为了保证操作的规范性(比如Reflect.set会返回布尔值表示修改成功,而直接赋值不会),同时与Proxy的拦截行为更匹配。
三、完整测试:验证响应式效果
我们已经实现了effect、track、trigger、reactive四个核心模块,现在编写测试代码验证响应式是否生效:
javascript
// 1. 创建普通对象并转为响应式对象
const user = reactive({ name: "张三", age: 20 });
// 2. 定义副作用函数(模拟组件渲染:依赖user.name和user.age)
effect(() => {
console.log(`姓名:${user.name},年龄:${user.age}`);
});
// 3. 修改响应式数据,观察副作用是否触发
user.name = "李四"; // 输出:姓名:李四,年龄:20(触发effect重新执行)
user.age = 21; // 输出:姓名:李四,年龄:21(再次触发effect)
user.gender = "男"; // 新增属性(Proxy天然支持,若有依赖该属性的effect也会触发)
运行结果:
- effect首次执行时,输出"姓名:张三,年龄:20"(初始渲染);
- 修改user.name时,触发set拦截→trigger→effect重新执行,输出更新后的内容;
- 修改user.age时,同样触发effect更新;
- 新增user.gender时,若后续有effect依赖该属性,修改时也会触发更新(本测试中无依赖,故无输出)。
四、核心细节补充与简化点说明
上面的实现是简化版响应式,Vue3的真实响应式系统更复杂,这里补充几个关键细节和简化点:
1. 简化点:未处理嵌套对象
当前reactive函数仅对顶层对象进行Proxy拦截,若对象属性是嵌套对象(如user = { info: { age: 20 } }),修改user.info.age不会触发响应式。解决方法是在get拦截时,对返回的value进行判断,若为对象则递归调用reactive:
javascript
// 优化reactive的get拦截
get(target, key) {
const value = Reflect.get(target, key);
track(target, key);
// 递归处理嵌套对象
return typeof value === 'object' && value !== null ? reactive(value) : value;
}
2. 简化点:未处理数组
Proxy天然支持数组拦截,比如修改数组的push、splice、索引等操作。只需在set拦截时,对数组的特殊操作(如push会新增索引)进行处理,确保trigger能正确触发。当前简化实现已支持数组的索引修改,比如:
javascript
const list = reactive([1, 2, 3]);
effect(() => {
console.log("数组:", list.join(','));
});
list[0] = 10; // 输出:数组:10,2,3(触发effect)
list.push(4); // 输出:数组:10,2,3,4(触发effect)
3. 真实Vue3的扩展:调度执行、computed、watch等
我们的实现仅覆盖了核心响应式逻辑,Vue3还在此基础上扩展了:
- 调度执行:effect支持传入scheduler选项,实现副作用的延迟执行、防抖、节流等;
- computed:基于effect实现缓存机制,只有依赖变化时才重新计算;
- watch:监听响应式数据变化,触发回调函数(支持立即执行、深度监听等);
- Ref:处理基本类型的响应式(通过封装对象实现,核心还是Proxy)。
五、总结
本文通过"effect封装副作用 → track/trigger管理依赖 → reactive基于Proxy拦截数据"的步骤,实现了一个简易的Vue响应式系统。核心逻辑可概括为:
effect执行时标记活跃状态,访问响应式数据触发get拦截,通过track收集"数据-属性-effect"依赖;修改数据触发set拦截,通过trigger找到对应依赖并重新执行effect,最终实现响应式更新。
理解这个核心逻辑后,再去学习Vue3的computed、watch等API的实现原理,就会变得非常轻松。建议你动手敲一遍代码,尝试修改和扩展(比如添加嵌套对象支持、调度执行),加深对响应式原理的理解。