人人都会AI编程

4.4 ref 与 reactive 底层实现差异

更新时间:2026-07-11

refreactive 是 Vue 3 响应式系统的两个核心入口,它们都能创建一个被 Vue 自动追踪变化的响应式数据。但它们在使用方式、适用场景和底层实现上有明显差异,理解这些差异有助于在开发中做出正确的选择,避免响应式失效的坑。

本质区别:是否是对象包装

  • reactive:接收一个对象(或数组),返回该对象的响应式代理。底层基于 Proxy 直接拦截对象属性的读取和赋值操作。你操作响应式对象就和操作普通对象一样,不需要 .value
  • ref:接收一个内部值(可以是任意类型),返回一个包含 value 属性的响应式对象。底层依赖 RefImpl 类,通过 get valueset value 来收集依赖和触发更新。对于引用类型,ref 内部会调用 reactive 进行深度响应式转换。

底层实现:Proxy vs. 自定义访问器

reactive 的实现 相对直接:

// 简化的 reactive 核心思路
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 依赖收集:track(target, key)
      const result = Reflect.get(target, key, receiver)
      // 如果结果是对象,则递归 reactive(延迟转换)
      return isObject(result) ? reactive(result) : result
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      if (oldValue !== value) {
        // 触发更新:trigger(target, key)
      }
      return result
    }
  })
}

Proxy 能够拦截包括 indeletefor...in 在内的多种对象操作,所以 reactive 可以天然支持属性的新增、删除等场景。

ref 的实现 则是一个类:

class RefImpl {
  constructor(value) {
    this._value = isObject(value) ? reactive(value) : value
    this._rawValue = value
  }
  get value() {
    // 读取时:track(this, 'value')
    return this._value
  }
  set value(newVal) {
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      // 如果是对象,则走 reactive 转换
      this._value = isObject(newVal) ? reactive(newVal) : newVal
      // 触发更新:trigger(this, 'value')
    }
  }
}

ref 的响应式依赖于对象属性的 getter / setter:当你在代码中读取或修改 ref.value 时,实际上触发了 RefImpl 中自定义的访问器逻辑,从而完成依赖收集和派发更新。对于对象类型,它内部会调用 reactive 来完成深层代理,所以 refreactive 在对象类型的响应式效果上是一致的。

基本类型响应式的实现路径不同

这一点是 ref 存在的根本原因。reactive 基于 Proxy,而 Proxy 只能代理对象。如果你需要让一个基本类型值(如字符串、数字、布尔值)变为响应式,必须把它包成一个对象,然后监听一个固定的属性(如 .value)的变化。ref 正是做了这件事:它将基本类型值包装成 { value: xxx } 的对象,然后通过自定义 getter/setter 实现响应式。

在实际使用中,这就决定了:

  • 基本类型值(数字、字符串、布尔)只能refreactive(true) 会直接报错。
  • 对象类型既可以用 reactive,也可以用 ref。但 ref 在操作时需要多一个 .value,而 reactive 可以直接访问属性。

模板中的解包行为

为了方便开发者,Vue 在模板中会对 ref 进行自动解包。也就是说,在模板里写 {{ count }} 而不是 {{ count.value }},Vue 会帮你自动取 value。但请注意,这只对顶层 ref 生效——如果 ref 是某个响应式对象的一个属性,模板中不再自动解包,需要显式写 .value 或在 setup 中使用 toRefs 处理。

赋值与解构的行为差异

  • reactive 对象:不能直接整体替换。比如 let state = reactive({ a: 1 }),后续写 state = { a: 2 } 会导致 state 失去响应式,因为这改变了变量引用,新的普通对象没有通过 Proxy 代理。同时,从 reactive 对象中解构出的属性会丢失响应式:
  const state = reactive({ count: 0 })
  let { count } = state  // count 现在是普通数字,不再响应式
  

需要用 toRefs() 来安全解构。

  • ref:由于响应式绑定在 .value 上,你可以通过修改 ref.value 来替换整个值,包括将一个普通对象赋给 .valueref 也会自动将其转换为深层响应式。解构不适用于 ref 本身(因为它只是一个对象),但通常我们不会去解构 ref

选择建议

| 场景 | 推荐 | 原因 |
|------|------|------|
| 基本类型(字符串、数字、布尔) | ref | reactive 无法接受基本类型,而 ref 是唯一选择。 |
| 单一大对象(如表单数据、配置对象) | reactive | 不需要 .value,代码更清爽,且解构需求少。 |
| 需要整体替换的数据(如接口返回的整个对象) | ref | 可以直接 ref.value = newData,无需担心响应式丢失。 |
| 多个独立状态组合 | reactive + toRefs 或组合多个 ref | 视逻辑组织需要,可结合 computed/watch 灵活组合。 |

简单总结:ref 是一个“包装器”,把任意值变成响应式对象,通过 .value 驱动;reactive 是一个“代理器”,直接让对象本身具有响应式。理解它们的底层差异,可以帮你准确选用,并从根源上避开响应式失效的常见坑。