Vue3 的 reactive 是实现响应式数据的核心 API 之一,它基于 Proxy 代理 实现,相比 Vue2 的 Object.defineProperty 具有更强的能力(支持数组、新增属性、Map/Set 等)。下面我会从核心原理、源码拆解、关键逻辑三个维度,由浅入深解析 reactive 的实现。
一、核心原理概述
reactive 的本质是:通过 Proxy 拦截目标对象的读取、修改、删除等操作,在读取时收集依赖(track),在修改时触发依赖更新(trigger) ,从而实现数据变化驱动视图更新。
二、源码核心拆解(简化版)
Vue3 源码中 reactive 相关逻辑主要在 packages/reactivity/src/reactive.ts 文件中,下面是关键代码的简化版(保留核心逻辑,剔除边界处理),方便你理解:
1. 核心常量与工具函数
// 1. 存储响应式对象的缓存(避免重复代理)
const reactiveMap = new WeakMap();
// 2. 标记响应式对象的唯一标识
export const ReactiveFlags = {
IS_REACTIVE: '__v_isReactive', // 标记是否为reactive对象
RAW: '__v_raw' // 指向原始对象
};
// 3. 判断是否为可代理的对象(排除基本类型、null等)
function isObject(value) {
return typeof value === 'object' && value !== null;
}
// 4. 判断是否为只读/非响应式对象(边界处理)
function canObserve(value) {
return !value[ReactiveFlags.IS_READONLY] && isObject(value);
}
2. reactive 主函数
export function reactive(target) {
// 边界1:如果目标是只读对象,直接返回
if (target && target[ReactiveFlags.IS_READONLY]) {
return target;
}
// 边界2:如果目标已经是响应式对象,直接返回代理对象
const existingProxy = reactiveMap.get(target);
if (existingProxy) {
return existingProxy;
}
// 边界3:非对象类型(如字符串/数字),无法代理,直接返回
if (!isObject(target)) {
return target;
}
// 核心:创建Proxy代理
const proxy = new Proxy(target, {
// 拦截读取操作(如 obj.xxx、obj['xxx']、in 操作、for...in 等)
get(target, key, receiver) {
// 特殊key:判断是否为reactive对象
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
// 特殊key:获取原始对象
if (key === ReactiveFlags.RAW) {
return target;
}
// 读取原始值(处理继承/原型链)
const res = Reflect.get(target, key, receiver);
// 收集依赖:只有非特殊key才收集
if (!isSymbol(key) && !isNonTrackableKeys(key)) {
track(target, 'get', key); // 核心:收集依赖
}
// 深度代理:如果返回值是对象,递归转为reactive
if (isObject(res)) {
return reactive(res);
}
return res;
},
// 拦截修改操作(如 obj.xxx = 123)
set(target, key, value, receiver) {
// 获取旧值,用于对比
const oldValue = Reflect.get(target, key, receiver);
// 如果是新增属性(旧值为undefined且key不在对象中)
const hadKey = hasOwn(target, key);
// 执行赋值操作
const result = Reflect.set(target, key, value, receiver);
// 避免重复触发:只有值真的变化才触发更新
if (!hadKey || !Object.is(oldValue, value)) {
trigger(target, 'set', key, value, oldValue); // 核心:触发更新
}
return result;
},
// 拦截删除操作(如 delete obj.xxx)
deleteProperty(target, key) {
const hadKey = hasOwn(target, key);
const result = Reflect.deleteProperty(target, key);
// 只有删除成功才触发更新
if (hadKey && result) {
trigger(target, 'delete', key);
}
return result;
},
// 拦截 in 操作(如 'xxx' in obj)
has(target, key) {
const result = Reflect.has(target, key);
// 收集依赖
if (!isSymbol(key)) {
track(target, 'has', key);
}
return result;
},
// 拦截 Object.keys/for...in 等遍历操作
ownKeys(target) {
track(target, 'iterate', Array.isArray(target) ? 'length' : ITERATE_KEY);
return Reflect.ownKeys(target);
}
});
// 缓存代理对象,避免重复创建
reactiveMap.set(target, proxy);
return proxy;
}
3. 依赖收集(track)与触发更新(trigger)核心逻辑
track 和 trigger 是响应式的灵魂,简化版逻辑如下:
// 存储依赖的映射表:target -> key -> 副作用函数集合
const targetMap = new WeakMap();
// 当前正在执行的副作用函数(如组件渲染函数、watch回调)
let activeEffect = null;
// 收集依赖
function track(target, type, key) {
// 无活跃副作用函数时,无需收集
if (!activeEffect) return;
// 1. 获取target对应的依赖映射
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 2. 获取key对应的副作用函数集合
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 3. 将当前副作用函数加入集合
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
// 反向收集:让副作用函数记住自己被哪些dep收集,用于清理
activeEffect.deps.push(dep);
}
}
// 触发更新
function trigger(target, type, key, newValue?, oldValue?) {
// 1. 获取target对应的依赖映射
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 2. 收集所有需要执行的副作用函数
const effects = new Set();
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => effects.add(effect));
}
};
// 3. 根据操作类型获取对应的副作用函数
if (key !== void 0) {
add(depsMap.get(key)); // 触发key对应的副作用
}
// 4. 执行所有副作用函数
effects.forEach(effect => {
if (effect.scheduler) {
effect.scheduler(); // 调度执行(如nextTick)
} else {
effect(); // 直接执行
}
});
}
三、关键细节解析
1. 为什么用 Proxy 而不是 Object.defineProperty?
- 支持数组:Proxy 能拦截数组的
push/pop/splice等操作(通过set拦截),而Object.defineProperty无法监听数组索引变化。 - 支持新增 / 删除属性:Proxy 能拦截
deleteProperty和新增属性的set操作,而Object.defineProperty只能监听已有属性。 - 支持 Map/Set 等集合类型:Proxy 可以拦截这些内置对象的方法(如
set/delete)。
2. 缓存机制(reactiveMap)
reactiveMap 是一个 WeakMap(弱引用,不影响垃圾回收),用于存储「原始对象 -> 代理对象」的映射,避免对同一个对象重复创建 Proxy
const obj = {};
const r1 = reactive(obj);
const r2 = reactive(obj);
console.log(r1 === r2); // true
3. 深度响应式
在 get 拦截器中,若读取的属性值是对象,则递归调用 reactive 转为响应式,实现深度代理。
4. 避免无限递归
通过 ReactiveFlags.RAW 标记原始对象,当代理对象的 get 操作读取 __v_raw 时,直接返回原始对象,避免代理对象访问自身导致的无限递归。
5. 边界处理
- 非对象类型(如字符串、数字)直接返回,不代理;
- 只读对象(
readonly创建的)不重复代理; - 只有值真正变化时(
!Object.is(oldValue, value))才触发更新,避免无效更新。
四、总结
reactive核心是通过 Proxy 拦截对象操作,在读取时track收集依赖,修改时trigger触发更新;- 利用
WeakMap(reactiveMap/targetMap)实现缓存和依赖管理,兼顾性能和垃圾回收; - 相比 Vue2 的响应式,
reactive支持数组、新增属性、复杂集合类型,且实现更简洁。
补充:与 ref 的区别
reactive用于对象 / 数组的响应式,基于 Proxy 实现;ref用于基本类型(如字符串、数字)的响应式,本质是封装成{ value: xxx }的对象,再通过reactive代理value属性。