在 Vue 3 的响应式系统中,ref 和 reactive 是开发者最常接触的两个 API。它们共同构成了 Vue 响应式能力的基础, 但设计上却有着根本性的差异。许多初学者会困惑:为什么需要两套 API?它们是否冗余?本节将从 Vue 3 源码实现的角度, 深入剖析 ref 与 reactive 的设计哲学,揭示其背后关于“深层响应式”与“引用包裹”的权衡与取舍。
一、核心差异:数据结构与访问方式
我们首先从使用层面观察两者的不同。
reactive 接收一个对象,并返回该对象的响应式代理(Proxy):
const state = reactive({ count: 0, name: 'Vue' });
state.count++; // 直接访问和修改属性ref 接收任意值(包括原始值),返回一个带有 .value 属性的响应式对象:
const count = ref(0);
count.value++; // 必须通过 .value 访问表面上看,reactive 更“自然”,而 ref 多了一层 .value 的访问。那么,为什么不能只用 reactive?答案在于 JavaScript 的语言限制。
二、JavaScript 的限制:Proxy 无法代理原始值
reactive 的实现依赖于 ES6 的 Proxy。Proxy 只能作用于“可代理对象”(Object、Array、Function 等), 无法代理原始值(number、string、boolean、null、undefined、symbol)。
这意味着,如果你尝试这样使用:
const count = reactive(0); // 错误:0 不是对象,无法被 Proxy 代理reactive 将抛出错误或静默失败。因此,对于原始值的响应式化,Vue 必须采用另一种策略。
ref 的实现正是为了解决这一问题。它的核心思想是:将原始值包装在一个对象中,然后对这个对象进行响应式处理。
从源码角度看,ref 的简化实现如下:
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:
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 不会自动将其深层响应式化。
const obj = ref({ count: 0 });
obj.value.count++; // 不会触发更新,除非 { count: 0 } 本身是响应式的如果需要深层响应式,你必须手动将对象用 reactive 包装,或使用 shallowRef 等变体。这种设计将控制权交给了开发者,避免了不必要的深层代理开销。
四、模板与 Composition API 的解耦需求
ref 的存在,还解决了 Composition API 与模板之间的数据传递问题。
在 setup 函数中,我们可能从多个自定义 Hook 中返回状态:
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 };
}
}在模板中,我们希望直接使用 count 和 user.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 的访问虽然多了一层,但它清晰地标识了“此处发生了响应式读写”。这在调试和代码审查时,是一种有益的“显式性”。
六、总结
ref 与 reactive 的并存,不是设计的冗余,而是对 JavaScript 语言限制、性能考量、开发体验和类型安全等多方面权衡的结果。
reactive基于Proxy,提供深层响应式,适用于对象和复杂数据结构,访问方式自然,但无法处理原始值。ref通过引用包裹,支持所有值类型,包括原始值,是 Composition API 中返回状态的通用容器,其.value提供了明确的响应式语义。
两者互补,共同构成了 Vue 3 灵活而强大的响应式系统。理解它们背后的实现机制和设计取舍,是掌握 Vue 3 响应式精髓的关键。