在 Vue3 中,ref 和 reactive 是 Composition API 的核心工具,用于创建响应式数据。尽管它们功能类似,但在使用场景、实现原理和实际应用中存在显著差异。本文将从面试和工作中的常见问题入手,深入探讨两者的区别、实现机制以及项目中的最佳实践,助你在 Vue3 开发中游刃有余。
一、核心差异对比
1.1 使用上的基本区别
- ref:适合定义任何类型的数据,包括基本类型(如数字、字符串)和引用类型(如对象、数组)。访问和修改数据时需通过 .value。
- reactive:仅适用于对象类型(对象或数组),不支持基本类型。直接操作对象的属性即可触发响应式更新。
// 基本类型
const count = ref(0); // ✅ 推荐
count.value = 1; // 修改值
const num = reactive(0); // ❌ 抛出警告,非对象类型无效
// 引用类型
const obj1 = reactive({ a: 1 }); // ✅ 直接操作属性
obj1.a = 2;
const obj2 = ref({ a: 1 }); // ✅ 通过 .value 操作
obj2.value.a = 2;
面试注意:如果简单回答“ref 用于基本类型,reactive 用于对象”,虽然不完全错,但不够全面。实际选择应基于赋值方式和使用场景。
1.2 赋值行为的差异
两者的关键区别在于直接赋值时的表现:
- reactive:直接赋值会破坏响应式。
- ref:通过 .value 赋值保持响应式。
// reactive 错误示例
let obj = reactive({});
setTimeout(() => {
obj = { a: 123 }; // ❌ 失去响应式,变为普通对象
console.log(obj); // { a: 123 }
}, 1000);
// ref 正确示例
const state = ref({});
setTimeout(() => {
state.value = { a: 123 }; // ✅ 保持响应式
console.log(state.value); // Proxy { a: 123 }
}, 1000);
工作注意:在请求数据时,通常会直接赋值(如 data = res),此时推荐使用 ref,避免 reactive 的响应式丢失问题。
1.3 修改数据的对比
对于对象属性的修改,两者都能保持响应式,但操作方式不同:
let obj1 = reactive({ a: 1 });
let obj2 = ref({ a: 1 });
setTimeout(() => {
obj1.a = 100; // ✅ 直接修改属性
obj2.value.a = 100; // ✅ 通过 .value 修改属性
}, 1000);
区别:ref 多了 .value 的访问层,而 reactive 更直观。
二、ref 的实现原理
2.1 RefImpl 核心代码
ref 的实现依赖于 RefImpl 类,以下是简化的实现:
class RefImpl {
constructor(value) {
this._rawValue = value; // 原始值
this._value = isObject(value) ? reactive(value) : value; // 对象转为 reactive
this.dep = new Set(); // 依赖收集容器
}
get value() {
trackRefValue(this); // 依赖收集
return this._value;
}
set value(newVal) {
if (hasChanged(newVal, this._rawValue)) { // 值变化时更新
this._rawValue = newVal;
this._value = isObject(newVal) ? reactive(newVal) : newVal;
triggerRefValue(this); // 触发更新
}
}
}
function ref(value) {
return new RefImpl(value);
}
实现步骤:
- 判断值是否为对象,若是则用 reactive 包装。
- 将值存储为 _value 属性。
- 通过 get 和 set 定义 value 属性,实现依赖收集和更新触发。
- 返回实例对象。
注意:shallowRef 不会将对象包装为 reactive,仅保持浅层响应式。
2.2 响应式触发机制
- ref 的 get 和 set:
- get value:访问 .value 时触发,收集依赖。
- set value:直接修改 .value(如 ref.value = xxx)时触发,仅在整体替换时生效。
- Proxy 的 get 和 set:
- 如果 ref 的值是对象,内部通过 reactive 转为 Proxy。
- 修改对象属性(如 ref.value.a = 1)触发 Proxy 的 set。
const obj = ref({ a: 1 });
obj.value.a = 2; // 触发 Proxy 的 set
obj.value = { b: 3 }; // 触发 RefImpl 的 set
三、常见现象与问题
3.1 为什么 ref 需要 .value?
- 直接赋值(如 ref = 123)会覆盖整个 RefImpl 实例,导致响应式丢失。
- .value 是 RefImpl 的属性,确保操作的是内部值。
const count = ref(0);
count = 1; // ❌ 失去响应式
count.value = 1; // ✅ 正确
3.2 ref 的对象是响应式的
ref 的值如果是对象,会自动转为 reactive,因此对象属性修改也能触发更新:
const obj = ref({ a: 1 });
obj.value.a = 2; // ✅ 触发响应式
浅层 ref 例外:shallowRef 不包装对象,属性修改无响应式。
四、是否只用 ref,不用 reactive?
4.1 可以只用 ref 吗?
理论上可以,因为:
- ref 支持所有类型。
- 对象类型内部会转为 reactive,功能覆盖 reactive。
const state = ref({ a: 1 });
state.value.a = 2; // 等价于 reactive
严格来说,没有绝对只能用 reactive 的场景。但在以下情况更适合用 reactive:
- 深层对象操作:直接操作多层属性时,reactive 更简洁。
- 避免 .value:团队规范或代码风格偏好时。
const state = reactive({ user: { name: 'Alice' } });
state.user.name = 'Bob'; // 简洁
4.3 项目中常用哪个?
- 简单数据:ref,如计数器、状态标志。
- 复杂对象:reactive,如表单数据、嵌套结构。
- 实际建议:根据赋值习惯选择,请求数据用 ref,内部修改用 reactive。
五、Proxy 的原理简解
reactive 基于 ES6 的 Proxy,拦截对象操作:
const reactiveHandler = {
get(target, key) {
track(target, key); // 依赖收集
return Reflect.get(target, key);
},
set(target, key, value) {
const oldValue = target[key];
Reflect.set(target, key, value);
if (hasChanged(value, oldValue)) {
trigger(target, key); // 触发更新
}
return true;
}
};
function reactive(obj) {
return new Proxy(obj, reactiveHandler);
}
- get:访问属性时收集依赖。
- set:修改属性时触发更新。
- 优势:无需像 Vue2 的 Object.defineProperty 手动定义每个属性。
六、复杂情况解析
6.1 给 ref 赋值 reactive
const data = reactive({ a: 1 });
const state = ref(data);
state.value.a = 2; // ✅ 保持响应式
注意:即使是 shallowRef,赋值 reactive 后仍保持响应式,shallow 无效。
6.2 给 reactive 赋值 ref
const count = ref(0);
const state = reactive({ num: count });
state.num.value = 1; // ✅ 仍为 ref 对象
用途:可用此方法让 reactive 包含基本类型。
七、企业级最佳实践
7.1 使用规范
- 基本类型:统一用 ref。
- 复杂对象:优先用 reactive。
- 组件 props:用 ref 风格。
- 全局状态:结合 Pinia 使用 reactive。
7.2 性能优化
- 大对象:用 shallowRef 减少开销。
- 高频更新:用 computed 缓存。
const bigData = shallowRef(largeObj);
bigData.value = { ...bigData.value, key: 'new' };