引言:从「盒子」到「幻象」的进化

        在 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 的场景:

  1. 基本数据类型(String, Number, Boolean等)
    这是 ref 的主场,你没有其他选择。
    const name = ref('张三');
    const age = ref(18);
    const isLoading = ref(false);
  2. 需要在未来替换整个对象引用时
    ref 因为总是通过 .value 访问,所以替换整个值非常安全。
    const user = ref({ name: 'Alice', age: 30 });
    
    // 从 API 获取新数据后,整个替换
    const fetchUser = async () => {
      const newData = await api.getUser();
      user.value = newData; // 完美替换,保持响应式
    };
  3. 在组合式函数(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 的场景:

  4. 逻辑紧密相关的多个属性组成的对象
    当一组属性属于同一个逻辑单元时,用 reactive 聚合它们更自然。
    // 表单状态
    const form = reactive({
      username: '',
      password: '',
      rememberMe: false
    });
    
    // 页面状态
    const pageState = reactive({
      data: null,
      loading: false,
      error: null
    });
    // 访问:pageState.loading,非常符合直觉
  5. 复杂的嵌套对象
    虽然 ref 也能包装对象,但对于深层嵌套的对象,使用 reactive 在代码书写上更简洁。
    const nestedData = reactive({
      user: {
        profile: {
          name: 'John',
          address: { city: 'New York' }
        }
      }
    });
    // 直接访问,无需 .value
    console.log(nestedData.user.profile.address.city);

  6. 组合使用
    // 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 响应式系统的强大能力。

        记住最好的选择标准:让代码更清晰、更易维护、更符合团队习惯的那个,就是正确的选择。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐