Skip to content

在 Vue 3 的响应式系统中,refreactive 是开发者最常接触的两个 API。它们共同构成了 Vue 响应式能力的基础, 但设计上却有着根本性的差异。许多初学者会困惑:为什么需要两套 API?它们是否冗余?本节将从 Vue 3 源码实现的角度, 深入剖析 refreactive 的设计哲学,揭示其背后关于“深层响应式”与“引用包裹”的权衡与取舍。

一、核心差异:数据结构与访问方式

我们首先从使用层面观察两者的不同。

reactive 接收一个对象,并返回该对象的响应式代理(Proxy):

text
const state = reactive({ count: 0, name: 'Vue' });
state.count++; // 直接访问和修改属性

ref 接收任意值(包括原始值),返回一个带有 .value 属性的响应式对象:

text
const count = ref(0);
count.value++; // 必须通过 .value 访问

表面上看,reactive 更“自然”,而 ref 多了一层 .value 的访问。那么,为什么不能只用 reactive?答案在于 JavaScript 的语言限制。

二、JavaScript 的限制:Proxy 无法代理原始值

reactive 的实现依赖于 ES6 的 ProxyProxy 只能作用于“可代理对象”(Object、Array、Function 等), 无法代理原始值(number、string、boolean、null、undefined、symbol)。

这意味着,如果你尝试这样使用:

text
const count = reactive(0); // 错误:0 不是对象,无法被 Proxy 代理

reactive 将抛出错误或静默失败。因此,对于原始值的响应式化,Vue 必须采用另一种策略。

ref 的实现正是为了解决这一问题。它的核心思想是:将原始值包装在一个对象中,然后对这个对象进行响应式处理

从源码角度看,ref 的简化实现如下:

text
function ref(value) {
  return {
    __v_isRef: true,
    get value() {
      track(this, 'value'); // 依赖收集
      return value;
    },
    set value(newVal) {
      value = newVal;
      trigger(this, 'value'); // 触发更新
    }
  };
}

这里,ref 返回一个普通对象,其 value 属性通过 getter/setter 实现了响应式追踪。原始值被闭包捕获并存储在 value 变量中。这种“引用包裹”的模式,使得原始值也能参与响应式系统。

三、深层响应式的设计取舍

reactive 的另一个关键特性是“深层响应式”(Deep Reactivity)。当你对一个对象调用 reactive,它不仅代理该对象本身,还会递归地将其所有嵌套属性也转换为响应式。

源码中,reactive 通过 createReactiveObject 创建 Proxy 实例,并在 get 拦截器中对返回的嵌套对象再次调用 reactive

text
const get = (target, key, receiver) => {
  const res = Reflect.get(target, key, receiver);
  // 如果获取的是一个对象,递归进行响应式处理
  if (isObject(res)) {
    return reactive(res);
  }
  track(target, key); // 依赖收集
  return res;
};

这种设计保证了无论数据结构多深,修改任何层级的属性都能触发更新。然而,这种“自动深层代理”也带来了性能和内存开销。每一次属性访问,只要返回的是对象, 就会触发一次 reactive 调用,可能创建新的 Proxy 实例。

相比之下,ref 的响应式是“浅层”的。ref 只代理其外层的 .value 属性。如果 .value 是一个对象,Vue 不会自动将其深层响应式化。

text
const obj = ref({ count: 0 });
obj.value.count++; // 不会触发更新,除非 { count: 0 } 本身是响应式的

如果需要深层响应式,你必须手动将对象用 reactive 包装,或使用 shallowRef 等变体。这种设计将控制权交给了开发者,避免了不必要的深层代理开销。

四、模板与 Composition API 的解耦需求

ref 的存在,还解决了 Composition API 与模板之间的数据传递问题。

setup 函数中,我们可能从多个自定义 Hook 中返回状态:

text
function useCounter() {
  const count = ref(0);
  const increment = () => count.value++;
  return { count, increment };
}

function useUser() {
  const user = reactive({ name: 'Alice' });
  return { user };
}

export default {
  setup() {
    const { count, increment } = useCounter();
    const { user } = useUser();
    return { count, increment, user };
  }
}

在模板中,我们希望直接使用 countuser.name,而不关心它们是 ref 还是 reactive 对象。

Vue 3 的模板编译器对此做了特殊处理:在模板中使用 ref 时,会自动解包(unwrap)其 .value

五、API 设计的哲学:统一接口 vs 明确语义

为什么 Vue 不统一为一种 API?例如,让 reactive 也能处理原始值,或者让 ref 在所有情况下都自动解包?

根本原因在于类型系统的清晰性和运行时的可预测性。

如果 reactive 被扩展为能处理原始值,那么它的返回类型将变得模糊:有时是 Proxy,有时可能需要返回一个包装对象。这会破坏类型推断,使 TypeScript 的类型系统难以准确描述其行为。

ref 的设计保持了类型一致性:无论输入是原始值还是对象,ref 的返回类型始终是 { value: T }。这使得 TypeScript 能够精确推断 ref 的类型,为开发者提供更好的开发体验。

此外,ref 明确地表达了“这是一个响应式引用”的语义。.value 的访问虽然多了一层,但它清晰地标识了“此处发生了响应式读写”。这在调试和代码审查时,是一种有益的“显式性”。

六、总结

refreactive 的并存,不是设计的冗余,而是对 JavaScript 语言限制、性能考量、开发体验和类型安全等多方面权衡的结果。

  • reactive 基于 Proxy,提供深层响应式,适用于对象和复杂数据结构,访问方式自然,但无法处理原始值。
  • ref 通过引用包裹,支持所有值类型,包括原始值,是 Composition API 中返回状态的通用容器,其 .value 提供了明确的响应式语义。

两者互补,共同构成了 Vue 3 灵活而强大的响应式系统。理解它们背后的实现机制和设计取舍,是掌握 Vue 3 响应式精髓的关键。