跳到主要内容

ref 与 reactive 的原理

refreactive 是 Vue 3 响应式系统里最核心的两个 API。很多人会用,但答题时容易停留在:

  • ref 用于基本类型
  • reactive 用于对象

这只是“使用建议”,不是原理。

更准确的理解是:

  • ref把一个值包进带 .value 的响应式壳里
  • reactive给对象创建 Proxy 代理,让对象属性访问参与依赖收集和触发更新

面试速答(30 秒版 TL;DR)

  • ref 的核心是一个 RefImpl 对象,内部通过 get value / set value 做依赖收集和触发。
  • reactive 的核心是 Proxy,在 gettrack,在 set / deletetrigger
  • 两者底层都接入同一套依赖系统,只是包装形态不同。
  • ref 更适合单值和需要整体替换的场景;reactive 更适合对象结构化状态。
  • 常见坑:解构 reactive 会丢响应式,模板里会自动解包 ref,但 JS 里不会。

1. ref 的原理

1.1 它为什么有 .value

因为 ref 返回的不是原始值本身,而是一个包装对象:

const count = ref(0)

可以粗略理解成:

const count = {
get value() {
track()
return innerValue
},
set value(newValue) {
innerValue = newValue
trigger()
},
}

这就是为什么:

  • count.value 时能收集依赖
  • count.value = 1 时能触发更新

1.2 为什么基本类型通常用 ref

因为基本类型没法直接被 Proxy 代理成“按属性访问追踪”的对象语义,所以 Vue 需要额外包一层壳。

2. reactive 的原理

reactive 会返回目标对象的代理:

const state = reactive({ count: 0 })

粗略理解:

const state = new Proxy(target, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key)
return result
},
})

这里的关键不是 Proxy 本身,而是:

  • target + key 为维度建立依赖关系
  • 某个属性被谁读过,就把对应 effect 记下来
  • 这个属性改了,就只通知相关 effect

3. 二者底层共享哪套依赖结构?

Vue 3 内部通常会用类似这样的结构来存依赖关系:

WeakMap<Target, Map<Key, Dep>>

可以理解为:

  1. 先定位到哪个响应式对象 target
  2. 再定位到哪个属性 key
  3. 最后找到依赖集合 Dep

所以不管是 reactive(state).count,还是某个 ref.value,本质都要落到:

  • track
  • trigger

这两条主线上。

4. 模板为什么能直接写 count,而不是 count.value

因为模板里有自动解包(unref)机制。

例如:

<template>
<p>{{ count }}</p>
</template>

模板编译和运行时会帮你处理 ref 的解包。

但在普通 JavaScript 里:

console.log(count.value)

仍然要自己写 .value

5. 为什么解构 reactive 会丢响应式?

const state = reactive({ count: 0 })
const { count } = state

这时的 count 只是一个普通值拷贝,不再经过原来的 Proxy get / set

所以解构后,依赖链断了。

解决方式一般是:

  • toRef(state, 'count')
  • toRefs(state)

这样得到的仍是响应式引用。

6. ref 包对象和 reactive 包对象有什么区别?

const a = ref({ count: 0 })
const b = reactive({ count: 0 })

区别主要在访问方式和替换语义:

  • a 要通过 a.value.count
  • b 直接 b.count
  • a.value = 新对象 很自然
  • b = 新对象 则会直接把变量指向改掉,不是原响应式对象更新

所以:

  • 需要整体替换对象时,ref 往往更自然
  • 需要长期操作同一个状态对象时,reactive 往往更直观

7. 怎么选?

场景更推荐
单个数字、字符串、布尔值ref
需要整体替换的对象ref
表单对象、配置对象、本地状态对象reactive
解构后仍想保留响应式toRef / toRefs

实践里更稳的一条经验

不是“基本类型一定 ref、对象一定 reactive”,而是:

  • 单值心智模型:优先 ref
  • 对象状态心智模型:优先 reactive

8. 常见坑

8.1 误以为模板自动解包等于 JS 自动解包

不是。模板里能省 .value,脚本里通常不行。

8.2 reactive 重新赋值

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

这不是“更新响应式对象”,而是把变量直接指向一个普通新对象。

8.3 滥用深层大对象

reactive 很方便,但如果把很大的业务对象整棵塞进去并频繁深度监听,成本依然会放大。

9. 面试高频答法

Q1:refreactive 的底层区别是什么?

ref 是一个带 value getter/setter 的包装对象,依赖收集和触发都围绕 .value 展开;reactive 是对对象做 Proxy 代理,在属性 gettrack,在 set/deletetrigger。两者底层依赖系统是共用的,只是包装入口不同。

Q2:为什么解构 reactive 会丢响应式?

:因为解构后拿到的是普通值,不再经过原 Proxy 的 get/set 拦截,自然也就无法继续收集依赖和触发更新。

速记要点

  • ref:包装单值,围绕 .value
  • reactive:Proxy 代理对象,围绕属性访问
  • 模板能自动解包,JS 通常不能
  • 解构 reactivetoRef / toRefs