在 Vue3 中,ref 和 reactive 是实现响应式数据的两个核心 API,均基于 @vue/reactivity 模块,但它们的使用场景、底层实现和特性存在显著差异。以下从核心区别、底层原理、使用场景等方面详细解析。
一、核心区别概览
| 维度 | ref |
reactive |
|---|---|---|
| 支持的数据类型 | 基本类型(number/string/boolean 等)、对象、数组 | 仅对象或数组(不支持基本类型) |
| 访问方式 | 通过 .value 访问 / 修改值 |
直接访问 / 修改属性(无需 .value) |
| 响应式实现 | 对基本类型:包装为带 .value 的对象;对对象:内部转为 reactive 代理 |
基于 Proxy 直接代理对象 / 数组 |
| 解构特性 | 解构后会丢失响应式(需用 toRefs 处理) |
解构后会丢失响应式(需用 toRefs 处理) |
| 适用场景 | 基本类型数据、单一值(如计数器、开关) | 复杂对象 / 数组(如用户信息、列表数据) |
二、底层实现原理
1. ref 的实现逻辑
ref 的核心是将数据包装为一个 带 .value 属性的响应式对象,源码简化逻辑如下:
typescript
function ref(value) {
// 创建一个包含 .value 的对象(RefImpl 实例)
const refObject = {
_value: convert(value), // 若 value 是对象,转为 reactive 代理
get value() {
track(this, 'get', 'value'); // 依赖收集
return this._value;
},
set value(newVal) {
newVal = convert(newVal); // 新值为对象时,转为 reactive
if (hasChanged(newVal, this._value)) {
this._value = newVal;
trigger(this, 'set', 'value'); // 触发更新
}
}
};
return refObject;
}
// 辅助函数:若值为对象,转为 reactive 代理
function convert(value) {
return isObject(value) ? reactive(value) : value;
}
-
对基本类型:
ref(1)会创建一个RefImpl实例,value的get/set被拦截,实现响应式。 -
对对象 / 数组:
ref({ name: 'foo' })内部会通过convert函数将对象转为reactive代理(即ref包裹的对象本质是reactive代理),此时.value指向该代理。
2. reactive 的实现逻辑
reactive 基于 ES6 Proxy 直接对对象 / 数组进行代理,源码简化逻辑如下:
typescript
function reactive(target) {
// 仅代理对象/数组,非对象直接返回
if (!isObject(target)) return target;
// 创建 Proxy 代理
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, 'get', key); // 依赖收集
// 若属性值是对象,递归代理(深层响应式)
return isObject(result) ? reactive(result) : result;
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
const result = Reflect.set(target, key, value, receiver);
if (hasChanged(value, oldValue)) {
trigger(target, 'set', key); // 触发更新
}
return result;
},
// 拦截 delete、has 等操作...
});
}
-
仅支持对象 / 数组:若传入基本类型(如
reactive(1)),会直接返回原值,不具备响应式。 -
深层响应式:访问对象的嵌套属性时(如
obj.a.b),会自动对嵌套对象创建Proxy代理,确保深层数据变化可被监听。
三、关键特性对比
1. 数据类型支持
-
ref:无类型限制,可接收基本类型(number、string、boolean、null、undefined)、对象、数组。例:const count = ref(0); const user = ref({ name: 'vue' }); -
reactive:仅支持对象或数组(包括Map、Set等复杂对象),传入基本类型时返回原值且无响应式。例:const user = reactive({ name: 'vue' }); // 有效;const count = reactive(0); // 无效,count 仍是 0。
2. 访问与修改方式
-
ref:必须通过.value访问或修改值(模板中使用时会自动解包,无需.value)。例:javascript
运行
const count = ref(0); console.log(count.value); // 0(脚本中必须用 .value) count.value = 1; // 修改值 const user = ref({ name: 'vue' }); user.value.name = 'vue3'; // 对象属性修改(.value 指向 reactive 代理)模板中自动解包:
<div>{{ count }}</div>(无需写count.value)。 -
reactive:直接访问或修改属性,无需.value。例:javascript
运行
const user = reactive({ name: 'vue' }); console.log(user.name); // 'vue' user.name = 'vue3'; // 直接修改属性
3. 解构与响应式丢失
-
ref和reactive共同问题:当对响应式对象解构时,会提取出原始值(非响应式),导致修改解构后的值无法触发更新。例:javascript
运行
// ref 解构 const count = ref(0); const { value: c } = count; // c 是原始值 0,修改 c 不影响 count // reactive 解构 const user = reactive({ name: 'vue' }); const { name } = user; // name 是原始值 'vue',修改 name 不影响 user -
解决方案:使用
toRefs将响应式对象转为 “ref集合”,确保解构后仍保持响应式:javascript
运行
import { reactive, toRefs } from 'vue'; const user = reactive({ name: 'vue', age: 3 }); const { name, age } = toRefs(user); // name 和 age 都是 ref 对象 name.value = 'vue3'; // 修改会触发更新,且同步到 user.name
4. 数组与集合的处理
-
ref处理数组:ref包裹数组时,.value指向数组的reactive代理,可直接通过.value操作数组方法(push、splice等),响应式有效。例:javascript
运行
const list = ref([1, 2, 3]); list.value.push(4); // 响应式有效,视图会更新 -
reactive处理数组:直接代理数组,可直接调用数组方法,响应式有效。例:javascript
运行
const list = reactive([1, 2, 3]); list.push(4); // 响应式有效 -
reactive处理Map/Set:支持对Map、Set的代理,但需注意:修改集合时需使用其原生方法(set、delete等),直接修改属性(如map.key = value)无效。例:javascript
运行
const map = reactive(new Map()); map.set('key', 'value'); // 有效,响应式 map.key = 'value'; // 无效,非响应式
四、使用场景建议
-
优先使用
ref的场景:- 存储基本类型数据(如计数器、开关状态、表单输入值)。
- 单一值的响应式(如
const visible = ref(false))。 - 不确定数据类型时(
ref兼容性更强)。
-
优先使用
reactive的场景:- 存储复杂对象或数组(如用户信息
{ name, age }、列表数据[{}, {}])。 - 希望直接访问属性(避免
.value繁琐)时。
- 存储复杂对象或数组(如用户信息
总结
ref 和 reactive 的核心差异在于 数据类型支持 和 访问方式:
-
ref是 “万能容器”,通过.value处理所有类型,适合基本类型和单一值。 -
reactive是 “对象代理”,直接操作属性,适合复杂对象 / 数组。
两者底层均依赖响应式系统(Proxy 和依赖收集),实际开发中可根据数据类型和使用习惯选择,配合 toRefs 解决解构响应式丢失问题。