vue里的ref和reactive的工作原理是什么?以及它们的区别是什么?为什么有人说reactive“废弃”了?
这是一个非常硬核且高质量的问题。要理解 Vue 3 的响应式系统,必须深入到源码层面。
首先,我需要纠正一个重要的误区:Vue 3 并没有“弃用” reactive 。它仍然是 Vue 3 核心 API 的一部分。但是,确实存在一种趋势(包括 Vue 官方团队的建议),推荐优先使用 ref 。
下面我将从源码级别(基于 Vue 3 packages/reactivity 模块)为你详细剖析两者的原理、区别,以及为什么现在大家更倾向于用 ref。
一、 reactive 的源码工作原理
reactive 的核心是利用了 ES6 的 Proxy 对象。它拦截对象的操作(增删改查),从而实现依赖收集和触发更新。
1. 核心流程 (reactive.ts)
当你调用 reactive(obj) 时,Vue 内部实际上执行了 createReactiveObject 函数。
简化版源码逻辑:
// 存放代理对象的缓存,防止同一个对象被代理多次
const reactiveMap = new WeakMap();
function createReactiveObject(target) {
// 1. 如果不是对象(是基础类型),直接返回,无法代理
if (!isObject(target)) {
return target;
}
// 2. 检查缓存,如果已经代理过,直接返回缓存的 Proxy
if (reactiveMap.has(target)) {
return reactiveMap.get(target);
}
// 3. 创建 Proxy
const proxy = new Proxy(target, mutableHandlers);
// 4. 存入缓存
reactiveMap.set(target, proxy);
return proxy;
}
2. 拦截器 (baseHandlers.ts)
Proxy 的威力在于第二个参数 mutableHandlers。它定义了 get(读取)和 set(修改)的拦截行为。
-
get (依赖收集) :当副作用函数(Effect,如 computed 或 render)读取属性时,触发
track函数,将当前 Effect 记录下来。 -
set (派发更新) :当修改属性时,触发
trigger函数,找到之前收集的 Effect 并执行它们。
简化版 Handler 逻辑:
const mutableHandlers = {
get(target, key, receiver) {
// 1. 收集依赖
track(target, key);
// 2. 获取原本的值
const res = Reflect.get(target, key, receiver);
// 3. 【深度响应关键点】如果获取到的 res 是对象,递归将其转为 reactive
// 这与 Vue 2 不同,Vue 3 是懒代理(访问时才代理),性能更好
if (isObject(res)) {
return reactive(res);
}
return res;
},
set(target, key, value, receiver) {
// 1. 获取旧值
const oldValue = target[key];
// 2. 设置新值
const result = Reflect.set(target, key, value, receiver);
// 3. 如果值发生变化,触发更新
if (hasChanged(value, oldValue)) {
trigger(target, key);
}
return result;
}
};
二、 ref 的源码工作原理
ref 的设计初衷是为了解决 基本数据类型(Primitives) 无法使用 Proxy 代理的问题(Proxy 只能代理对象)。
1. 核心流程 (ref.ts)
ref 本质上是一个 对象的包装器。它通过定义一个类 RefImpl,利用 ES6 的类属性访问器(getter/setter)来拦截 .value 的访问。
简化版源码逻辑:
function ref(value) {
return createRef(value);
}
function createRef(rawValue) {
if (isRef(rawValue)) return rawValue;
return new RefImpl(rawValue);
}
class RefImpl {
public _value; // 存储当前值
public _rawValue; // 存储原始值(用于比较)
public dep; // 依赖容器
public __v_isRef = true; // 标记这是一个 Ref
constructor(value) {
this._rawValue = value;
// 核心差异点:如果传入的是对象,内部会自动调用 reactive()!
this._value = isObject(value) ? reactive(value) : value;
}
get value() {
// 1. 收集依赖
trackRefValue(this);
return this._value;
}
set value(newVal) {
// 2. 检查值是否变化
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
// 如果新赋的值是对象,再次转为 reactive
this._value = isObject(newVal) ? reactive(newVal) : newVal;
// 3. 触发更新
triggerRefValue(this);
}
}
}
重点总结:
- 如果
ref(10):利用RefImpl的get value和set value进行拦截。 - 如果
ref({ count: 1 }):RefImpl会将内部的value变成reactive({ count: 1 })的 Proxy 对象。
三、 Ref 与 Reactive 的关键区别
| 特性 | Ref | Reactive |
|---|---|---|
| 数据类型 | 支持所有类型(基本类型 + 对象)。 | 仅支持对象(Array, Object, Map, Set)。 |
| 底层原理 |
RefImpl 类(getter/setter)。如果是对象,内部转调 reactive。 |
直接使用 Proxy。 |
| 访问方式 | 必须通过 .value 访问(模板中自动解包除外)。 |
直接访问属性。 |
| 重新赋值 |
myRef.value = {} 依然保持响应式。 |
let state = reactive({}); state = {} 会丢失响应性。 |
| 结构解构 | 解构会丢失响应性(需用 toRefs)。 |
解构会丢失响应性(需用 toRefs)。 |
四、 为什么说 Vue 3 “想弃用” reactive(实际上是推荐 ref)?
这是一个由 “开发者体验(DX)” 驱动的趋势。虽然 reactive 并没有被官方删除,但社区和尤雨溪(Evan You)都倾向于 “Ref 一把梭” ,主要原因如下:
1. reactive 的局限性会导致 Bug
Vue 新手最常遇到的坑就是 reactive 丢失响应性:
-
赋值替换问题:
let list = reactive([]); // 错误!这样赋值会切断 Proxy 的连接,页面不会更新 list = [1, 2, 3]; // 正确写法(很麻烦) list.push(...[1, 2, 3]); // 或者再包一层 const state = reactive({ list: [] }); state.list = [1, 2, 3];而
ref没有这个问题:list.value = [1, 2, 3]总是安全的。 -
解构丢失问题:
const state = reactive({ count: 0 }); let { count } = state; // count 变成了普通数字,不再是响应式的
2. 类型不一致带来的心智负担
在一个组件中,如果混用两者:
- 有些变量直接读(reactive)。
- 有些变量要加
.value(ref)。 - 有些对象是通过
props传进来的,你不知道它是普通对象还是 Proxy。
使用 ref 可以统一心智模型:
- 在 JS 代码中,凡是响应式数据,都要带
.value。这种显式调用虽然写起来多几个字符,但让代码更清晰,更容易区分哪些是普通变量,哪些是响应式变量。
3. 官方的态度
尤雨溪曾在社区讨论中表示,如果 Vue 3 重新设计,也许会只保留 ref。虽然 reactive 为了 API 的完整性被保留了,但在实际业务开发中,全量使用 ref 是目前最推荐的最佳实践。
总结
-
Reactive: 基于
Proxy,只对对象有效,存在解构和重新赋值丢失响应性的风险。 -
Ref: 基于
class的 getter/setter,统一了基本类型和对象的处理(对象内部调用 reactive)。 -
结论: 建议在项目中优先使用
ref。它更加稳健,不易出错,且能够清晰地标识出“这是一个响应式数据”。