续:Vue 3 的 Reactive 与 Ref:如何选择?
引言:从「盒子」到「幻象」的进化
在 Vue 3 的响应式宇宙中,我们面临着两个核心选择:ref 和 reactive。前者如同一个精心设计的「包装盒」,后者则像是创造了一个「数字幻象」。理解它们的内在差异,是掌握 Vue 3 响应式系统的关键一步。
在上一篇文章中,我们揭开了 ref 和 .value 的神秘面纱,知道了它是一个用于包装任何类型的“通用盒子”。那么,Vue 3 中专门为对象准备的 reactive 又是如何工作的?为什么有了 ref,我们还需要 reactive?在实际开发中,我到底该用哪个?
今天,我们就来彻底解决这些疑问。
一、Reactive 的魔法:基于 Proxy 的透明代理
如果说 ref 是一个“盒子”,那么 reactive 更像一个“魔法泡泡”,它包裹在一个对象(或数组)外面,让你几乎感觉不到它的存在,却能监听到对象内部发生的一切变化。
1、重新理解「代理」的本质
让我们用一个更准确的比喻来理解 Proxy:Proxy 不是「中间人」,而是「全息投影」
想象一下:
-
原始对象:一个真实的苹果
-
Proxy 代理对象:这个苹果的全息投影
-
你:观察者
当你与全息投影互动时:
const realApple = { color: 'red', weight: 100 }; // 真实苹果
const hologram = reactive(realApple); // 全息投影
// 你「看起来」是在直接操作投影:
hologram.color = 'green';
hologram.weight = 150;
// 但实际上,你的操作被「映射」到了真实苹果上
console.log(realApple.color); // 'green' - 真实苹果也变了!
关键洞察:Proxy 创造的是一个「幻象对象」——它看起来、用起来都和原对象一模一样,但所有操作都被暗中监控和重定向。
2、为什么 Reactive 不需要 .value?代码对比揭秘
让我们从代码层面看看这个「幻象」是如何工作的:
Ref 的工作方式:「盒子模型」
// Ref 创建明确的包装层
const countRef = ref(0);
// 内部结构示意
{
_value: 0, // 实际数据存储
get value() {
track(this, 'value'); // 依赖收集
return this._value;
},
set value(newVal) {
this._value = newVal;
trigger(this, 'value'); // 触发更新
}
}
// 必须通过 .value 访问
countRef.value = 1; // 显式「开箱」操作
Reactive 的工作方式:「幻象模型」透明代理
// Reactive 创建透明代理
const state = reactive({ count: 0 });
// 底层 Proxy 实现
const proxy = new Proxy(target, {
get(target, key) {
track(target, key); // 隐式依赖追踪
return target[key]; // 直接返回值
},
set(target, key, value) {
target[key] = value; // 直接设置值
trigger(target, key); // 隐式触发更新
return true;
}
});
// 直接操作,无感知代理
state.count = 1; // 自然的对象操作体验
核心区别:
-
ref是包装器:你操作的是包装器本身(需要.value) -
reactive是替身:你操作的是替身,替身直接映射到真实对象 -
// Reactive 创建透明代理 const state = reactive({ count: 0 }); // 底层 Proxy 实现 const proxy = new Proxy(target, { get(target, key) { track(target, key); // 隐式依赖追踪 return target[key]; // 直接返回值 }, set(target, key, value) { target[key] = value; // 直接设置值 trigger(target, key); // 隐式触发更新 return true; } }); // 直接操作,无感知代理 state.count = 1; // 自然的对象操作体验这就是为什么不需要 .value:Proxy 代理对象在行为上与原始对象完全一致,JavaScript 语言本身无法区分它们。如果你对源代码有兴趣的话,我把核心 Proxy 处理器(最重要的文件!)地址放在下面:
-
https://github.com/vuejs/core/blob/main/packages/reactivity/src/baseHandlers.ts
正因为这种“透明代理”的特性,你在使用 reactive 对象时,不需要 .value,可以直接像操作普通对象一样操作它,体验非常直观。
二、Ref 与 Reactive 的对比:一张图看懂区别
| 维度 | Ref | Reactive | 优势分析 |
|---|---|---|---|
| 创建语法 | ref(value) |
reactive(object) |
Reactive 更贴近原生对象创建 |
| 访问方式 | 需要 .value |
直接属性访问 | Reactive 代码更简洁 |
| 模板使用 | 自动解包 | 直接使用 | 两者在模板中体验一致 |
| 数据类型 | 支持所有类型 | 仅对象类型 | Ref 适用范围更广 |
| 引用替换 | 支持完整替换 | 替换丢失响应式 | Ref 更适合动态数据结构 |
| TypeScript | 完善类型推断 | 深度类型支持 | Reactive 对象类型更精确 |
三、实战选择指南:什么时候用 Ref?什么时候用 Reactive?
这是核心问题。没有绝对的“最好”,只有“更合适”。以下是基于实践总结的指南。
优先使用 ref 的场景:
-
基本数据类型(String, Number, Boolean等)
这是ref的主场,你没有其他选择。const name = ref('张三'); const age = ref(18); const isLoading = ref(false); -
需要在未来替换整个对象引用时
ref因为总是通过.value访问,所以替换整个值非常安全。const user = ref({ name: 'Alice', age: 30 }); // 从 API 获取新数据后,整个替换 const fetchUser = async () => { const newData = await api.getUser(); user.value = newData; // 完美替换,保持响应式 }; -
在组合式函数(Composables)中返回值时
使用ref可以保持一致性,让调用方明确知道这是一个响应式变量,需要通过.value操作。// composable/useCounter.js export function useCounter(initialValue = 0) { const count = ref(initialValue); const increment = () => { count.value++; }; return { count, increment }; // 返回 ref,调用方结构后仍是 ref }优先使用
reactive的场景: -
逻辑紧密相关的多个属性组成的对象
当一组属性属于同一个逻辑单元时,用reactive聚合它们更自然。// 表单状态 const form = reactive({ username: '', password: '', rememberMe: false }); // 页面状态 const pageState = reactive({ data: null, loading: false, error: null }); // 访问:pageState.loading,非常符合直觉 -
复杂的嵌套对象
虽然ref也能包装对象,但对于深层嵌套的对象,使用reactive在代码书写上更简洁。const nestedData = reactive({ user: { profile: { name: 'John', address: { city: 'New York' } } } }); // 直接访问,无需 .value console.log(nestedData.user.profile.address.city); -
组合使用
// Ref 用于独立状态,Reactive 用于聚合状态 const searchQuery = ref(''); // 独立搜索词 const filters = ref({ // 可替换的筛选条件 category: '', priceRange: [0, 1000] }); const uiState = reactive({ // 界面状态聚合 isSearching: false, searchResults: [], selectedResult: null }); // 响应式计算基于两者 const hasResults = computed(() => searchQuery.value && uiState.searchResults.length > 0 );
总结:
在实际项目中,两种方案往往并存。明智的开发者不会拘泥于单一选择,而是根据具体场景灵活运用,充分发挥 Vue 3 响应式系统的强大能力。
记住最好的选择标准:让代码更清晰、更易维护、更符合团队习惯的那个,就是正确的选择。
更多推荐
所有评论(0)