reactive:使用 Proxy 代理对象。
ref:使用一个包含 _value 属性的普通对象(RefImpl),通过 ES6 Class 的 get/set 访问器手动调用 track 和 trigger,不使用 Object.defineProperty,也不对 Ref 本身套 Proxy。
依赖管理:使用 WeakMap 存储依赖关系,支持嵌套对象和自动解包。
为了更直观地理解 ref 和 reactive 的底层运行逻辑,这里提供一份简化版的手写实现代码。
🚀 简易响应式系统完整实现
/**
* ==========================================
* 1. 依赖收集与触发系统 (Dependency System)
* ==========================================
*/
// 全局状态:存储当前正在执行的副作用函数
let activeEffect = null;
// 依赖映射表:Target -> Key -> Set<Effects>
// 使用 WeakMap 防止内存泄漏(当对象被垃圾回收时,依赖自动清除)
const targetMap = new WeakMap();
/**
* 依赖收集:当读取数据时,将当前 effect 记录到依赖表中
* @param {Object} target - 目标对象
* @param {String|Symbol} key - 属性键
*/
function track(target, key) {
if (!activeEffect) return; // 没有正在运行的 effect,无需收集
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep0 = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 避免重复添加同一个 effect
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
}
}
/**
* 触发更新:当数据修改时,执行所有依赖该数据的 effect
* @param {Object} target - 目标对象
* @param {String|Symbol} key - 属性键
*/
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
if (effects) {
// 创建副本执行,防止 effect 内部修改数据导致无限循环或集合修改错误
new Set(effects).forEach(effect => effect());
}
}
/**
* 注册副作用函数
* @param {Function} fn - 副作用函数
*/
function effect(fn) {
const wrappedEffect = () => {
try {
activeEffect = wrappedEffect; // 设置当前激活的 effect
return fn(); // 执行函数,期间会触发 getter 从而调用 track
} finally {
activeEffect = null; // 执行完毕,重置
}
};
wrappedEffect(); // 立即执行一次以收集初始依赖
return wrappedEffect;
}
/**
* ==========================================
* 2. Reactive 实现 (基于 Proxy)
* ==========================================
*/
/**
* 将对象转换为响应式代理
* @param {Object} obj
* @returns {Proxy}
*/
function reactive(obj) {
// 仅处理非空对象
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 如果已经是响应式对象,直接返回(避免重复代理)
if (obj.__v_isReactive) return obj;
const handler = {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// 【核心】收集依赖
track(target, key);
// 【核心】深层响应:如果获取的值是对象,递归转为 reactive
// 注意:这里只在访问时才转换,属于懒代理
if (typeof res === 'object' && res !== null) {
return reactive(res);
}
return res;
},
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;
},
deleteProperty(target, key) {
const hadKey = key in target;
const result = Reflect.deleteProperty(target, key);
if (hadKey) {
trigger(target, key);
}
return result;
}
};
const proxy = new Proxy(obj, handler);
// 标记为响应式对象,用于判断
Object.defineProperty(proxy, '__v_isReactive', {
value: true,
enumerable: false,
writable: false
});
return proxy;
}
/**
* ==========================================
* 3. Ref 实现 (基于 Class Getter/Setter)
* ==========================================
*/
/**
* 辅助函数:将值转换为响应式(如果是对象则用 reactive,否则保持原样)
*/
function convert(val) {
return typeof val === 'object' && val !== null ? reactive(val) : val;
}
/**
* Ref 类:包装基础类型或对象
* Vue 3 中 ref 并不使用 defineProperty,也不对 Ref 实例本身做 Proxy
* 而是通过类的 get/set 访问器手动调用 track/trigger
*/
class RefImpl {
constructor(rawValue) {
this._rawValue = rawValue; // 保存原始值
this._value = convert(rawValue); // 保存转换后的值(对象会被转为 reactive)
this.__v_isRef = true; // 标记为 Ref
}
get value() {
// 【核心】读取 .value 时,收集对 'value' 这个 key 的依赖
// 注意:这里的 target 是 RefImpl 实例本身
track(this, 'value');
return this._value;
}
set value(newVal) {
if (newVal !== this._rawValue) {
this._rawValue = newVal;
this._value = convert(newVal);
// 【核心】修改 .value 时,触发对 'value' 这个 key 的更新
trigger(this, 'value');
}
}
}
/**
* 创建 ref
* @param {*} value
* @returns {RefImpl}
*/
function ref(value) {
return new RefImpl(value);
}
/**
* 判断是否为 ref
*/
function isRef(r) {
return !!(r && r.__v_isRef);
}
/**
* 解包 ref:在模板或 reactive 中自动解包
*/
function unref(refObj) {
return isRef(refObj) ? refObj.value : refObj;
}
💡 代码运行演示与解析
1. 测试 reactive (对象代理)
const state = reactive({ count: 0, user: { name: 'Alice' } });
effect(() => {
console.log(`[Reactive Effect] Count is: ${state.count}, Name is: ${state.user.name}`);
});
// 输出: [Reactive Effect] Count is: 0, Name is: Alice
state.count++;
// 输出: [Reactive Effect] Count is: 1, Name is: Alice
state.user.name = 'Bob';
// 输出: [Reactive Effect] Count is: 1, Name is: Bob
2. 测试 ref (基础类型包装)
const countRef = ref(10);
effect(() => {
console.log(`[Ref Effect] CountRef is: ${countRef.value}`);
});
// 输出: [Ref Effect] CountRef is: 10
countRef.value = 20;
// 输出: [Ref Effect] CountRef is: 20
3. 测试 ref 包裹对象 (自动转换)
const userRef = ref({ name: 'Vue' });
effect(() => {
// 注意:userRef 是 RefImpl 实例,访问 .value 拿到的是 reactive 代理对象
// 所以 userRef.value.name 能触发响应式
console.log(`[Ref Object Effect] User is: ${userRef.value.name}`);
});
// 输出: [Ref Object Effect] User is: Vue
userRef.value.name = 'React';
// 输出: [Ref Object Effect] User is: React
// 整体替换 ref 的对象
userRef.value = { name: 'Svelte' };
// 输出: [Ref Object Effect] User is: Svelte
🔑 核心差异总结(通过代码视角)
| 对比维度 | reactive 代码实现细节 | ref 代码实现细节 |
|---|---|---|
| 底层核心机制 | 直接通过 new Proxy(target, handler) 代理目标对象,完全基于 ES6 Proxy 拦截能力 |
基于 ES6 Class 定义 RefImpl 类,通过类的 get value() / set value() 访问器实现拦截,不使用 Proxy 包装 Ref 实例本身 |
| 依赖收集触发点 | 在 Proxy 的 get 陷阱中自动执行 track(target, key),key 为对象的具体属性名 |
在类的 get value() 方法中手动调用 track(this, 'value'),key 固定为字符串 'value' |
| 更新触发逻辑 | 在 Proxy 的 set 陷阱中自动执行 trigger(target, key),仅属性值真正变化时触发 |
在类的 set value() 方法中手动调用 trigger(this, 'value'),仅新旧值不等时触发 |
| 支持数据类型 | 仅支持对象、数组等引用类型,传入基础类型直接原值返回 | 全类型兼容,基础类型直接包装,对象类型内部自动调用 reactive 完成转换 |
| 深层响应实现 | 懒代理模式:在 get 陷阱中递归调用 reactive(res),访问嵌套属性时才完成代理 |
复用 reactive 能力:通过内部 convert 函数,将传入的对象自动转为 reactive 代理实现深层响应 |
| 整体替换行为 | 无法直接整体替换代理对象,赋值新对象会断开 Proxy 链接,响应式完全失效 | 支持整体替换:直接给 .value 赋值新对象,新值会自动走响应式转换流程,响应式完整保留 |
| 内存标记方式 | 给 Proxy 实例挂载不可枚举属性 __v_isReactive 标记身份 |
直接在 RefImpl 类的构造函数中挂载 __v_isRef 属性标记身份 |
| 初始化性能表现 | 仅代理外层对象,嵌套对象访问时才代理,初始化开销低 | 基础类型无额外代理开销,对象类型初始化时仅做一次 reactive 转换,整体性能更轻量 |
在 Vue 3 开发中,选择 ref 还是 reactive 主要取决于数据类型、代码维护习惯以及业务场景的复杂度。
虽然两者都能实现响应式,但为了代码的可读性、一致性和避免常见的“响应式丢失”坑点,业界目前更倾向于优先使用 ref。以下是具体的选型建议和最佳实践:
🏆 核心结论:优先使用 ref
Vue 官方文档及社区主流观点建议:在大多数场景下,统一使用 ref 是更合理的选择。
理由:ref 语法统一(无论基础类型还是对象都用 .value),避免了在 ref 和 reactive 之间切换思维模式的认知负担,且彻底规避了 reactive 解构丢失响应式、整体替换失效等痛点。
📊 详细选型指南
1. 必须/推荐使用 ref 的场景
| 场景 | 原因与示例 |
|---|---|
| 基础数据类型 (String, Number, Boolean) |
reactive 不支持基础类型。若强行使用 reactive(1) 会报警告且无响应式。✅ const count = ref(0) |
| DOM 或组件实例引用 | 模板中的 ref="xxx" 属性必须配合脚本中的 ref(null) 使用,用于获取 DOM 元素或子组件实例。✅ const inputRef = ref(null) |
| 需要频繁解构或传递单个属性 | ref 本身就是独立的响应式引用,解构或作为参数传递时不会丢失响应式。✅ const { name } = toRefs(state) 或直接传递 ref 变量。 |
| 希望代码逻辑高度统一 | 团队规范中若希望所有响应式数据都通过 .value 访问,以减少“这个变量要不要加 .value”的判断成本。✅ const user = ref({ name: 'Vue' }) |
| 需要整体替换对象 | 当你需要用一个新对象完全替换旧对象时,ref 能完美保持响应式链接,而 reactive 会断裂。✅ state.value = newObj (有效)❌ state = newObj (无效,丢失响应式) |
2. 可以考虑使用 reactive 的场景
| 场景 | 原因与示例 |
|---|---|
| 大型复杂嵌套对象/表单 | 当数据结构非常深,且你希望像操作普通 JS 对象一样直接通过 obj.prop 访问,不想到处写 .value 时。✅ const form = reactive({ user: { name: '', age: 0 } }) 访问: form.user.name |
| 与第三方库交互 | 某些第三方库期望接收一个纯对象引用,而不是一个包含 .value 的包装器。此时使用 reactive 更自然。 |
| 性能极度敏感且对象巨大 | 虽然差异极小,但 ref 包裹对象时内部仍有一层包装。对于极大对象,直接使用 reactive 代理原对象可能在微观层面少一层间接引用(通常可忽略)。 |
⚠️ 注意:如果使用 reactive,请务必配合 toRefs 进行解构,否则解构后的变量将失去响应式。
const state = reactive({ name: 'Vue', age: 3 })
// ❌ 错误:name 失去响应式
const { name } = state
// ✅ 正确:保留响应式
const { name, age } = toRefs(state)
💡 避坑对照表
| 操作 | ref 表现 |
reactive 表现 |
建议 |
|---|---|---|---|
| 定义基础类型 | ✅ 正常响应 | ❌ 警告且无响应 | 必选 ref |
| 定义对象/数组 | ✅ 正常响应 (内部转 reactive) | ✅ 正常响应 | 推荐 ref (统一风格) |
| 脚本中取值 | 需加 .value |
直接访问属性 | ref 需多敲几个字符,但更明确 |
| 模板中取值 | 自动解包,无需 .value |
直接访问属性 | 两者在模板中体验一致 |
| 解构赋值 | ✅ 保持响应式 (本身就是 ref) | ❌ 丢失响应式 | reactive 必须用 toRefs |
| 整体替换 | ✅ obj.value = {} 保持响应 |
❌ obj = {} 丢失响应 |
推荐 ref |
| 获取 DOM | ✅ 唯一标准方式 | ❌ 不支持 | 必选 ref |
🚀 企业级落地最佳实践建议
默认策略:项目中默认全部使用 ref。
优点:心智模型统一,不用判断类型;解构安全;替换安全;DOM 引用方便。
缺点:脚本中需要频繁写 .value(可通过 IDE 插件或 ESLint 规则辅助提示)。
例外策略:仅在以下情况使用 reactive:
你正在处理一个巨大的、深层嵌套的表单对象,且团队约定好绝不解构该对象,始终通过 form.field.subField 方式访问。
你需要将响应式对象传递给一个不接受 Ref 包装器的遗留代码或第三方库。
工具函数搭配:
使用 unref() 或 toValue() (Vue 3.3+) 来统一处理可能是 ref 也可能是普通值的参数。
使用 isRef() 和 isReactive() 进行调试或类型守卫。
总结一句话:除非你有明确的理由(如避免大量 .value 书写且保证不解构),否则无脑选 ref 是最安全、最现代、最易维护的选择。
