跳到主要内容

ref 与 reactive:从用法到原理,一次讲透

在 Vue 3 的 Composition API 里,refreactive 是你每天都会用到的两个响应式 API,但也最容易产生困惑:

  • 为什么 ref.value
  • reactive 不能包基本类型吗?
  • reactive 一解构就“失效”是怎么回事?
  • 表单、列表、对象到底该用哪个更合适?

这篇文章会用通俗直觉 + 大量例子 + 常见坑,把 refreactive 讲清楚。

核心结论(先背下来)

能用 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.usernameform.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:到底怎么选?

下面这张表把最关键的差异列出来(面试也常问):

维度refreactive
支持的数据类型✅ 任意(基本类型/对象/数组)✅ 仅对象(含数组/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 → track
  • state.count = 1:触发 set → trigger
学会这句就够用

refgetter/setter 追踪,reactiveProxy 追踪;两者最终目的都是一样的: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. 常见坑速查(看到就能立刻定位问题)

  1. 脚本里忘记 .valuecount++ 不生效(或直接报错)
  2. reactive 直接解构:const { a } = state 导致 a 不更新
  3. reactive([ref(x)]) 以为会自动解包:其实数组索引不会
  4. 想替换整个 reactive 对象:写法别扭、容易丢引用,改用 ref({})
  5. watch(state, ...) 触发频率太高:改用 watch(() => state.xxx, ...) 精准监听

10. 面试高频问答(背这些基本够用)

Q1:refreactive 的核心区别是什么?

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;状态对象用 reactivereactive 要解构就 toRefs;需要浅层追踪就 shallowRef/shallowReactive