Vue 3 响应式核心:Ref 与 Reactive
在 Vue 2 中,我们习惯了将所有数据都塞进 data() 选项里,Vue 内部通过 Object.defineProperty 悄悄地帮我们处理好了一切。但在 Vue 3 的 Composition API 中,Vue 将响应式系统的控制权交还给了开发者。我们需要显式地定义哪些数据是“响应式”的(即数据变了,视图自动更新)。
Vue 3 提供了两个主要的工具来做这件事:ref 和 reactive。它们背后的底层机制完全不同(Ref 基于 Object wrapper,Reactive 基于 ES6 Proxy)。
很多新手会问:“为什么不只用一个?它们有什么区别?怎么选才不会踩坑?”
这篇文章将带你彻底搞懂这两个 API 的原理、陷阱与最佳实践。
- Ref:万能的包装器
ref (Reference) 是最灵活、最常用的响应式 API。它的设计初衷是为了解决 JavaScript 中“基本数据类型”无法被追踪引用的问题。
1.1 核心原理:为什么需要 .value?
在 JavaScript 中,基本类型(如 0, "hello", false)是按值传递的。如果你把一个数字传递给函数,或者赋值给变量,你传递的是这个值的“副本”,而不是它的“引用”。这意味着 Vue 无法追踪这个数字的变化。
为了解决这个问题,ref 像一个箱子(Wrapper),把这个值装了起来:
// 伪代码:ref 的内部实现逻辑
function ref(value) {
return {
_value: value,
get value() {
track(); // 告诉 Vue:有人在看这个数据
return this._value;
},
set value(newValue) {
this._value = newValue;
trigger(); // 告诉 Vue:数据变了,去更新视图!
}
}
}
1.2 适用范围与用法
适用范围:任何类型。你可以传数字、字符串,也可以传对象、数组。如果是对象,ref 内部会自动调用 reactive 进行深层代理。
JS 中访问:必须通过 .value。这虽然麻烦,但显式地提醒你“这是一个响应式引用”。
模板中访问:Vue 做了自动解包处理,不需要写 .value。
const count = ref(0);
const user = ref({ name: 'Alice' });
console.log(count.value); // 0
count.value++; // ✅ 修改必须用 .value
// 修改对象内部属性,依然是响应式的
user.value.name = 'Bob';
- Reactive:对象的原生代理
reactive 是另一种定义响应式数据的方式,它更接近 Vue 2 的 data 对象的行为,但在底层完全重写了。
2.1 核心原理:ES6 Proxy
reactive 基于 ES6 的 Proxy 对象。它不创建一个“箱子”包装数据,而是直接创建一个原始对象的代理拦截器。当你访问或修改对象的属性时,代理会拦截这个操作,并通知 Vue 更新视图。
const raw = {};
const proxy = new Proxy(raw, handler); // reactive 返回的就是这个 proxy
2.2 适用范围与限制
适用范围:只能是对象类型(Object, Array, Map, Set)。绝对不能用于基本类型(String, Number, Boolean),因为 Proxy 无法代理基本值。
JS 中访问:像操作普通对象一样直接访问属性,不需要 .value,读写体验非常丝滑。
const state = reactive({ count: 0, items: [] });
console.log(state.count); // 0
state.count++; // ✅ 直接修改
state.items.push('New Item'); // ✅ 数组变异方法也能触发更新
2.3 致命陷阱:重新赋值导致响应性丢失
这是 reactive 最容易导致 Bug 的地方。因为 reactive 返回的是一个 Proxy 对象,如果你把这个对象替换成一个新的普通对象,响应性链条就断了。
let state = reactive({ count: 0 });
// ❌ 致命错误:这会切断引用,state 变成了一个普通的、没有响应性的对象
state = { count: 1 };
// ✅ 正确做法 1:修改属性,而不是替换对象
state.count = 1;
// ✅ 正确做法 2:使用 Object.assign 保持引用
Object.assign(state, { count: 1 });
- 核心区别与进阶技巧
既然 reactive 不需要写 .value,看起来更方便,为什么社区更推荐 ref?
这就涉及到了 解构 (Destructuring) 和 组合性 的问题。
3.1 解构丢失响应性问题
如果你把 reactive 对象里的属性解构出来,它们就变成了普通的本地变量,Vue 将无法追踪它们的变化。
const user = reactive({ name: 'Alice', age: 25 });
// ❌ 错误示范:解构
let { name } = user;
// 此时 name 只是一个字符串 'Alice',跟 user 对象毫无关系了。
name = 'Bob'; // 视图绝不会更新!
// ✅ 正确示范:始终保持对象引用
user.name = 'Bob';
3.2 解决方案:toRefs 神器
Vue 提供了 toRefs 辅助函数,它可以把一个 reactive 对象的所有属性都转换成独立的 ref。这样你就可以安全地解构了。
import { reactive, toRefs } from 'vue';
const user = reactive({ name: 'Alice', age: 25 });
// 将属性转换为 refs
const { name, age } = toRefs(user);
// 此时 name 是一个 ref,需要用 .value 修改,但它依然连接着 user 对象
name.value = 'Bob';
console.log(user.name); // 输出 'Bob',源对象同步更新!
最佳实践建议:
新手推荐:一把梭全用 ref。虽然要多写 .value,但它没有“重新赋值”和“解构丢失”的陷阱,心智负担最小,更安全。
进阶用法:当你有一组高度聚合的状态(比如复杂的表单数据、配置对象)时,使用 reactive 会让代码更整洁。如果需要解构返回给模板,记得包裹一层 toRefs。
- 实战案例:用户信息编辑器
我们将构建一个功能更完善的编辑器,演示 ref 的直接使用,以及如何配合 toRefs 优雅地使用 reactive。
4.1 逻辑代码 (JavaScript)
const { createApp, ref, reactive, toRefs } = Vue;
createApp({
setup() {
// --- 1. 使用 ref 定义独立状态 ---
const likes = ref(0);
const incrementLikes = () => {
// ref 必须通过 .value 修改
likes.value++;
};
// --- 2. 使用 reactive 定义聚合状态 ---
const userState = reactive({
name: '张三',
age: 25,
history: []
});
const updateProfile = () => {
// reactive 不需要 .value
userState.age++;
userState.history.push(`Age updated to ${userState.age}`);
};
// --- 3. 关键:使用 toRefs 保持响应性并解构 ---
// 这样在模板里就可以直接用 {{ name }} 而不是 {{ userState.name }}
// 同时保留了响应性
const userRefs = toRefs(userState);
return {
likes,
incrementLikes,
updateProfile,
// 将解构后的 refs 展开返回
...userRefs
};
}
}).mount('#app');
4.2 模板代码 (HTML Snippet)
这是对应的模板部分。注意观察我们是如何混用 ref 和解构后的 reactive 属性的。
<hr>
<!-- 场景 2: Reactive + toRefs 解构绑定 -->
<div class="section">
<span class="label">用户资料:</span>
<!--
因为使用了 toRefs 并展开返回,
这里可以直接访问 name 和 age,不需要 userState.name
-->
<div class="value">
{{ name }} - {{ age }}岁
</div>
<div class="input-group">
<label>修改姓名:</label>
<input v-model="name" placeholder="输入新名字">
</div>
<button @click="updateProfile">🎂 过生日 (+1岁)</button>
<!-- 展示 reactive 中的数组 -->
<ul>
<li v-for="(log, index) in history" :key="index">{{ log }}</li>
</ul>
</div>
- 总结与速查表
特性
ref
reactive
底层原理
Object.defineProperty (包装对象)
Proxy (原生代理)
数据类型
万能 (值类型 + 引用类型)
仅限对象 (Object, Array, Map)
JS 中访问
需要 .value (如 count.value)
直接访问 (如 state.count)
模板中访问
自动解包 (不需要 .value)
直接访问
重新赋值
✅ x.value = ... 安全
❌ x = ... 导致响应性丢失
解构
作为一个整体传递,安全
❌ 丢失响应性 (需用 toRefs)
TypeScript
类型推导更友好 (Ref
类型有时候会比较复杂
终极建议:
不用纠结。默认使用 ref。只有当你非常确定你需要一个大的对象容器,且你清楚 Proxy 的工作原理时,再使用 reactive。

浙公网安备 33010602011771号