ref 与 reactive 的原理
ref 和 reactive 是 Vue 3 响应式系统里最核心的两个 API。很多人会用,但答题时容易停留在:
ref用于基本类型reactive用于对象
这只是“使用建议”,不是原理。
更准确的理解是:
ref:把一个值包进带.value的响应式壳里reactive:给对象创建 Proxy 代理,让对象属性访问参与依赖收集和触发更新
面试速答(30 秒版 TL;DR)
ref的核心是一个RefImpl对象,内部通过get value/set value做依赖收集和触发。reactive的核心是Proxy,在get时track,在set/delete时trigger。- 两者底层都接入同一套依赖系统,只是包装形态不同。
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>>
可以理解为:
- 先定位到哪个响应式对象
target - 再定位到哪个属性
key - 最后找到依赖集合
Dep
所以不管是 reactive(state).count,还是某个 ref.value,本质都要落到:
tracktrigger
这两条主线上。
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.countb直接b.counta.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:ref 和 reactive 的底层区别是什么?
答:ref 是一个带 value getter/setter 的包装对象,依赖收集和触发都围绕 .value 展开;reactive 是对对象做 Proxy 代理,在属性 get 时 track,在 set/delete 时 trigger。两者底层依赖系统是共用的,只是包装入口不同。
Q2:为什么解构 reactive 会丢响应式?
答:因为解构后拿到的是普通值,不再经过原 Proxy 的 get/set 拦截,自然也就无法继续收集依赖和触发更新。
速记要点
ref:包装单值,围绕.valuereactive:Proxy 代理对象,围绕属性访问- 模板能自动解包,JS 通常不能
- 解构
reactive用toRef/toRefs