一、 Vue 3 响应式原理剖析:ref 与 reactive 的深度区别

在 Vue 3 的 Composition API 中,refreactive 是构建响应式数据的基石。理解它们的底层差异,能帮助你避免许多莫名其妙的 Bug。


1. 核心特性对比

特性 ref reactive
接受类型 任意类型 (基本类型 + 引用类型) 仅限引用类型 (Object/Array/Map/Set)
访问方式 脚本中需 .value,模板中自动解包 直接访问属性
重新赋值 支持修改 .value(响应式不丢失) 不支持直接替换对象(会丢失响应式)
解构 直接解构会丢失响应式 直接解构会丢失响应式(需配合 toRefs
底层实现 RefImpl 类 + 访问器属性 (getter/setter) Proxy 代理对象

2. 底层原理剖析

reactive:基于 Proxy 的代理

reactive 的核心是 ES6 的 Proxy。它直接拦截整个对象的操作。

  • 工作机制:当你访问或修改属性时,Vue 会通过 Proxy 的 getset 拦截器进行依赖收集(Track)和派发更新(Trigger)。
  • 局限:Proxy 只能代理对象。如果你传入一个字符串或数字,它无法拦截这些值的变化。

ref:对象的“包装盒”

ref 是为了解决基本类型(String, Number 等)无法响应式而设计的。

  • 核心矛盾:ES6 的 Proxy 只能代理 对象,无法代理 基本类型(如数字 0)。

为了统一操作接口,让基本类型也能拥有响应式能力,Vue 必须人为地制造一个对象(即 RefImpl 的实例),并将值挂载到 .value 属性上。

  1. 基本类型:Vue 将其封装成一个名为 RefImpl 的类实例。
    • 通过 get value() 收集依赖。
    • 通过 set value() 触发更新。
  2. 对象类型:如果你给 ref 传了一个对象,它内部会自动调用 reactive
    • 即:ref(obj) ≈ \approx new RefImpl(reactive(obj))
// 简化版的 RefImpl 伪代码
class RefImpl {
  constructor(value) {
    this._value = value;
  }

  get value() {
    track(this); // 收集依赖
    return this._value;
  }

  set value(newValue) {
    if (hasChanged(newValue, this._value)) {
      this._value = newValue;
      trigger(this); // 触发更新
    }
  }
}

3. 为什么 reactive 不能直接赋值?

这是 reactive 最典型的“坑”。

let state = reactive({ count: 0 });

// ❌ 错误操作:这会导致响应式丢失
state = { count: 1 };

reactive 返回的是原始对象的 Proxy 代理引用。当你执行 state = { … } 时,变量 state 指向了内存中一个全新的普通对象,而 Vue 追踪的是原来的那个代理地址。
相比之下,ref 允许 count.value = {},因为它修改的是 RefImpl 实例内部的属性,实例本身的引用没变。

二、Vue 3 响应式侦听器:watch vs watchEffect

在 Vue 3 中,watchwatchEffect 都是核心的响应式侦听工具,但它们的设计哲学完全不同。简单来说:watch 是“精确制导”,watchEffect 是“自动追踪”


1. 核心区别对照表

特性 watch watchEffect
副作用追踪 手动指定(显式声明侦听源) 自动追踪(回调里用了谁就侦听谁)
触发时机 默认懒执行(数据变了才跑) 立即执行(初始化时先跑一次)
访问旧值 支持(提供 oldValue) 不支持(只能拿到当前值)
精确度 (可精确控制触发源) (内部任何变量变化都会触发)

2. watch:精细化的指挥官

watch 需要你明确告诉它:“盯着这个变量,变了就告诉我。”

  • 惰性:除非设置 { immediate: true },否则在组件初始化时不会执行。
  • 对比性:它是唯一能让你对比“之前是多少,现在是多少”的手段。
  • 支持多种源:可以侦听 refreactive 对象的属性、甚至是一个 getter 函数。
const count = ref(0);

// 只有当 count 变化时才会触发
watch(count, (newValue, oldValue) => {
  console.log(`${oldValue} 变成了 ${newValue}`);
});

3. watchEffect:高效的打工人

watchEffect 不需要你指定谁,它会像一个“扫描仪”,自动识别回调函数中用到的响应式数据。

  • 自动依赖收集:不需要显式传递依赖源。

  • 立即执行:代码运行到这一行,立刻就会跑一遍(为了收集依赖)。

  • 代码更简洁:适合逻辑中涉及多个变量,且只要其中一个变了就重新运行的情况。

更多推荐