ref 与 reactive:从用法到原理,一次讲透
在 Vue 3 的 Composition API 里,ref 和 reactive 是你每天都会用到的两个响应式 API,但也最容易产生困惑:
- 为什么
ref要.value? reactive不能包基本类型吗?reactive一解构就“失效”是怎么回事?- 表单、列表、对象到底该用哪个更合适?
这篇文章会用通俗直觉 + 大量例子 + 常见坑,把 ref 与 reactive 讲清楚。
能用 ref 就用 ref(尤其是基本类型、需要整体替换的对象/数组、需要解构传递的值);当你有一坨“状态对象”想按 state.xxx 这种方式组织时,再用 reactive。
1. ref:把值装进“响应式盒子”
ref() 返回的是一个对象(Ref),真实值在 .value 里。
1.1 基本类型:ref 的主场
import { ref } from 'vue'
const count = ref(0)
count.value++ // ✅ 触发更新
在模板里会自动“解包”(不用写 .value):
<template>
<button @click="count++">{{ count }}</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
模板里可以 count++,但在 <script setup> 里仍然要写 count.value++(除非你额外开启过“响应式语法糖/宏”,这不是默认能力)。
1.2 对象/数组:ref 也能用,而且很常用
import { ref } from 'vue'
const user = ref({ name: 'Tom', age: 18 })
user.value.age++
user.value.name = 'Jerry'
你还可以整体替换(这点很关键):
user.value = { name: 'Spike', age: 20 } // ✅ 很自然
1.3 DOM / 组件引用:ref 的另一种“ref”
ref 不仅能做数据响应式,也常用来拿 DOM 或组件实例:
<template>
<input ref="inputEl" />
<button @click="focus">聚焦</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const inputEl = ref<HTMLInputElement | null>(null)
function focus() {
inputEl.value?.focus()
}
</script>
2. reactive:把对象变成“可追踪的代理”
reactive() 只能接收对象类型(普通对象、数组、Map/Set 等),返回一个 Proxy 代理对象。
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: { name: 'Tom' },
list: [1, 2, 3],
})
state.count++
state.user.name = 'Jerry'
state.list.push(4)
2.1 reactive 的一个特点:更像“状态对象”
当你有很多字段,喜欢集中管理时:
const form = reactive({
username: '',
password: '',
remember: true,
})
这种写法的可读性很高:form.username、form.password,很像在写传统的 data()。
2.2 reactive 的限制:不适合“整体替换”
你通常会这样写:
const state = reactive({ items: [] as string[] })
但如果你想整体替换 items,还好:
state.items = ['a', 'b'] // ✅ 替换某个字段 OK
真正麻烦的是:想整体替换 state 本身。
// ❌ 不推荐:你很难“替换整个 state”并让引用处都跟着变
// state = reactive({ ... })
如果你明确需要整体替换一整个对象(例如:请求回来新对象直接覆盖),通常用 ref({}) 会更顺手。
3. ref vs reactive:到底怎么选?
下面这张表把最关键的差异列出来(面试也常问):
| 维度 | ref | reactive |
|---|---|---|
| 支持的数据类型 | ✅ 任意(基本类型/对象/数组) | ✅ 仅对象(含数组/Map/Set) |
| 访问方式 | 脚本里要 .value | 直接 state.xxx |
| 整体替换 | ✅ x.value = newValue 很自然 | ❌ 不擅长替换整个对象 |
| 解构使用 | ✅ 解构后依然可用(ref 本身) | ❌ 解构会丢响应式(需 toRefs) |
| 更适合 | 单值、可替换对象、跨函数传递 | “一坨状态对象”、类似 data() 的组织方式 |
不纠结:默认 ref;只有当你真的想要一个“状态对象”(state.xxx 很多字段)时,再用 reactive。
4. reactive 一解构就“失效”?原因与正确姿势
这是新人最常踩的坑之一。
4.1 错误示例:直接解构 reactive
import { reactive } from 'vue'
const state = reactive({ count: 0 })
// ❌ count 只是一个普通 number 了
const { count } = state
setInterval(() => {
state.count++
console.log('state.count =', state.count)
console.log('count =', count)
}, 1000)
原因很简单:reactive() 返回的是 Proxy,响应式能力在“代理对象本身”上。你把 count 解构出来,就变成了一个普通值,当然不会再跟着更新。
4.2 正确姿势:toRefs / toRef
把每个字段转成 ref,再解构:
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Tom' })
const { count, name } = toRefs(state)
count.value++
name.value = 'Jerry'
只拿一个字段时用 toRef 更省:
import { reactive, toRef } from 'vue'
const state = reactive({ count: 0 })
const count = toRef(state, 'count')
如果你发现自己经常写 toRefs(state),那说明你可能更适合一开始就用多个 ref 来组织状态。
5. “解包”规则:为什么有时不用 .value?
ref 的 .value 之所以让人困惑,本质是因为 Vue 在不同场景做了“自动解包”(unwrapping)。
5.1 模板中:ref 会自动解包
<template>
<p>{{ count }}</p>
<button @click="count++">+1</button>
</template>
5.2 reactive 对象里:嵌套 ref 也会自动解包(但有例外)
import { reactive, ref } from 'vue'
const state = reactive({
count: ref(0),
})
state.count++ // ✅ 这里不用 .value
但请注意:数组和原生集合类型(Map/Set)里不会自动解包,这是面试/排查 bug 的高发点:
import { reactive, ref } from 'vue'
const arr = reactive([ref(0)])
arr[0].value++ // ✅ 需要 .value
const map = reactive(new Map([['count', ref(0)]]))
map.get('count')!.value++ // ✅ 需要 .value
6. 深响应式 vs 浅响应式:shallowRef / shallowReactive
默认情况下:
ref({}):内部对象会被转成响应式(“深”)reactive({}):同样是深响应式
当你只希望追踪“最外层变化”,例如把某个第三方库实例塞进响应式系统(不想深层递归代理),就用浅响应式。
6.1 shallowRef:只追踪 .value 的替换
import { shallowRef, triggerRef } from 'vue'
const cfg = shallowRef({ theme: { color: 'red' } })
cfg.value.theme.color = 'blue' // ❌ 不会触发更新
triggerRef(cfg) // ✅ 手动触发一次
cfg.value = { theme: { color: 'green' } } // ✅ 替换会触发更新
6.2 shallowReactive:只追踪第一层属性
import { shallowReactive } from 'vue'
const state = shallowReactive({
theme: { color: 'red' },
})
state.theme.color = 'blue' // ❌ 不触发(theme 是普通对象)
state.theme = { color: 'green' } // ✅ 替换第一层会触发
7. 简化版原理:ref/ reactive 是如何“追踪与触发”的?
你不需要背源码,但需要知道关键概念:依赖收集(track) 和 触发更新(trigger)。
7.1 ref 为什么要 .value?
因为 Vue 可以在 .value 的 getter/setter 里做拦截:
- 读取
.value:收集依赖(谁在用我) - 设置
.value:触发更新(通知所有用我的人)
7.2 reactive 为什么不需要 .value?
因为 reactive 返回的是 Proxy,Vue 可以在 Proxy 的 get/set 拦截里做同样的事情:
state.count:触发get→ trackstate.count = 1:触发set→ trigger
ref 用 getter/setter 追踪,reactive 用 Proxy 追踪;两者最终目的都是一样的:track & trigger。
8. 实战选型:常见场景怎么写更顺手?
- 计数器、开关、loading:
ref - 表单(字段多、喜欢
form.xxx):reactive - 列表数据(经常整体替换):
ref([])更顺手 - 请求返回一个对象并整体覆盖:
ref({})更顺手 - 需要大量解构传参/跨函数传递:优先
ref,或对reactive使用toRefs
一个更“工程化”的写法示例(推荐你模仿):
import { ref, reactive, toRefs } from 'vue'
// 单值:ref
const loading = ref(false)
const keyword = ref('')
// 多字段:reactive
const form = reactive({
username: '',
password: '',
})
// 需要解构时:toRefs
const { username, password } = toRefs(form)
9. 常见坑速查(看到就能立刻定位问题)
- 脚本里忘记
.value:count++不生效(或直接报错) reactive直接解构:const { a } = state导致a不更新reactive([ref(x)])以为会自动解包:其实数组索引不会- 想替换整个
reactive对象:写法别扭、容易丢引用,改用ref({}) watch(state, ...)触发频率太高:改用watch(() => state.xxx, ...)精准监听
10. 面试高频问答(背这些基本够用)
Q1:ref 和 reactive 的核心区别是什么?
ref 用于任意值,通过 .value 追踪;reactive 只用于对象类型,通过 Proxy 追踪。工程上:默认用 ref,状态对象用 reactive。
Q2:为什么 ref 在脚本里必须 .value,但模板里不用?
脚本里 ref 是对象,真实值在 .value;模板编译阶段会帮你做“自动解包”,所以模板里可以直接用。
Q3:reactive 为什么解构后会丢响应式?怎么解决?
响应式能力在 Proxy 上,解构拿到的是普通值。解决:toRefs(state) / toRef(state, 'xxx'),或者直接用多个 ref。
Q4:ref({}) 和 reactive({}) 都能做对象响应式,怎么选?
需要整体替换对象/跨函数传递/解构:ref({}) 更顺手;想用 state.xxx 风格集中管理字段:reactive({}) 更清晰。
Q5:reactive 里嵌套 ref 会怎样?
在普通对象属性上会自动解包:state.count++ 可直接用;但在数组索引或 Map/Set 里不会自动解包,需要 .value。
Q6:shallowRef / shallowReactive 用来解决什么问题?
避免深层递归代理(性能/兼容第三方实例),只追踪最外层变更;深层修改需要 triggerRef 或替换整个值。
Q7:如何让 watch 只监听某个字段,而不是整个对象?
用 getter:watch(() => state.count, (n, o) => {}),比 watch(state, ...) 更精准,触发也更可控。
Q8:什么时候“必须”用 toRefs?
当你要把 reactive 的字段解构出来使用(或传给别的函数/组件),又希望保持响应式时,就用 toRefs/toRef。
Q9:ref 的对象内部是响应式的吗?
通常是的:ref({}) 会把对象转成响应式代理,所以改 user.value.name 也会触发更新;但你仍然需要通过替换 .value 来整体替换对象。
Q10:一句话总结选型策略?
默认 ref;状态对象用 reactive;reactive 要解构就 toRefs;需要浅层追踪就 shallowRef/shallowReactive。