Vue 3 中 ref 和 reactive 区别完全指南
本文档专门讲解 Vue 3 里 ref 和 reactive 的区别:各自是什么、怎么用、何时用谁、常见坑与替代写法,配有大量对比示例,适合新手搞清“到底选哪个”。
一、为什么会有 ref 和 reactive 两种?
1.1 共同目标:让数据“响应式”
在 Vue 里,只有响应式数据变了,视图才会自动更新。
ref 和 reactive 都是用来把数据变成响应式的,但用法和适用场景不同。
1.2 设计上的分工
- ref:来自 “reference”(引用),可以包任意类型(数字、字符串、布尔、对象、数组等)。在 script 里要通过 .value 读写。
- reactive:来自 “reactive”(响应式的),只接受对象类型(普通对象、数组等)。在 script 里直接改属性,不需要 .value。
下面从“怎么用”开始,再对比“区别”和“怎么选”。
二、ref 的用法回顾
2.1 基本用法
ref(初始值) 返回一个响应式引用。
在 里:读用 xxx.value,写用 xxx.value = 新值。
在 里:自动解包,直接用 xxx,不写 .value。
<template>
<p>{{ count }}</p>
<button @click="count++">+1</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function add() {
count.value++
}
</script>
- 模板里写 count 即可(Vue 自动解包 ref)。
- script 里必须写 count.value。
2.2 ref 可以包任何类型
const n = ref(0)
const s = ref('hello')
const ok = ref(true)
const obj = ref({ name: 'a' })
const arr = ref([1, 2, 3])
- obj.value 才是对象,改 obj.value.name = ‘b’ 会触发更新。
- arr.value 才是数组,arr.value.push(4) 会触发更新。
2.3 整体替换:ref 可以“换一整份”
ref 的 .value 可以整体替换,替换后仍然是响应式的:
const user = ref({ name: '小明', age: 18 })
user.value = { name: '小红', age: 20 }
这样 user 仍然是一个 ref,只是里面包的对象换了,视图会正常更新。
三、reactive 的用法回顾
3.1 基本用法
reactive(对象) 会把整个对象变成响应式的。
在 script 里直接改属性,不需要 .value;在模板里也是直接写属性名。
<template>
<p>{{ state.count }}</p>
<button @click="state.count++">+1</button>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({
count: 0,
name: ''
})
</script>
- state 本身不是“值”,而是一个响应式对象,所以没有 .value。
- 只能写 state.count、state.name,不能写 state = xxx(见下文“坑”)。
3.2 只接受对象类型
reactive 只接受对象(包括数组)。传数字、字符串会报警告,应改用 ref:
reactive(0) // 不推荐,可能报警告
reactive('hello') // 不推荐
reactive({}) // 可以
reactive([]) // 可以
3.3 不能整体替换
给 reactive 变量重新赋值会断掉响应式,视图不会再更新:
const state = reactive({ count: 0 })
state = reactive({ count: 1 }) // 错误!state 变成了新的引用,原来的响应式丢了
state.count = 1 // 正确:只改属性
若业务上经常要“换一整份对象”,用 ref 包对象更合适。
四、核心区别对比表
| 对比项 | ref | reactive |
|---|---|---|
| 可接受类型 | 任意(数字、字符串、对象、数组等) | 仅对象(含数组) |
| script 里读写 | 必须用 .value | 直接属性,无 .value |
| template 里 | 自动解包,写 xxx | 写 state.xxx(属性名) |
| 整体替换 | 可以:xxx.value = 新值 | 不可以,会丢响应式 |
| 解构 | 解构出来仍是 ref,保留响应式 | 解构会丢响应式,需 toRefs |
| 传给别人/函数 | 传的是 ref,对方要 .value | 传的是对象,对方直接改属性 |
下面逐条用示例说明。
五、区别一:类型限制
ref:任意类型
const a = ref(0)
const b = ref('')
const c = ref(true)
const d = ref({ x: 1 })
const e = ref([1, 2, 3])
reactive:仅对象
const state = reactive({ count: 0 })
const list = reactive([1, 2, 3])
若需要一个“单独的”数字或字符串,用 ref;若是一组相关字段组成的对象,可以用 reactive 或 ref 包对象。
六、区别二:script 里是否要 .value
ref:必须要 .value
const count = ref(0)
count.value++
console.log(count.value)
const user = ref({ name: 'a' })
user.value.name = 'b'
reactive:直接属性,没有 .value
const state = reactive({ count: 0, name: '' })
state.count++
state.name = 'hello'
console.log(state.count)
state 没有 .value,state 本身就是那个响应式对象。
七、区别三:整体替换(能否重新赋值)
ref:可以整体换
const user = ref({ name: '小明', age: 18 })
user.value = { name: '小红', age: 20 }
user 还是同一个 ref,只是里面包的对象换了,响应式不丢。
reactive:不能整体换
let state = reactive({ count: 0 })
state = reactive({ count: 1 })
这里 state 被赋成了新的 reactive 对象,和模板里用的原来的 state 已经不是同一个引用了,所以原来的 state 不再和视图关联,响应式就断了。
正确做法是只改属性:
state.count = 1
若业务上经常要“整份替换”(例如接口返回一整份用户信息要覆盖),用 ref 包对象更合适:
const state = ref({ count: 0 })
state.value = { count: 1 }
八、区别四:解构后是否还响应式
ref:解构后仍是 ref
const count = ref(0)
const { value: countValue } = count
一般不这样解构 ref,通常直接 count.value。若把 count 传给子组件或 composable,传的是 ref 本身,对方改 count.value 仍然响应式。
reactive:解构会丢响应式(重要)
从 reactive 里解构出来的变量是普通变量,和响应式“脱钩”了:
const state = reactive({ count: 0, name: '' })
const { count, name } = state
count++ // 改的是普通变量,界面不会更新!
name = 'hello' // 同样不会更新
要让“解构出来的”仍然响应式,要用 toRefs 转成 ref 再解构:
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: '' })
const { count, name } = toRefs(state)
count.value++
name.value = 'hello'
这样 count、name 是 ref,改 .value 会同步回 state,视图会更新。
模板里可以继续写 count、name(自动解包)。
九、区别五:在模板里的写法
ref:自动解包,写变量名
<template>
<p>{{ count }}</p>
</template>
<script setup>
const count = ref(0)
</script>
模板里写 count,不写 count.value。
reactive:写对象 + 属性名
<template>
<p>{{ state.count }}</p>
<p>{{ state.name }}</p>
</template>
<script setup>
const state = reactive({ count: 0, name: '' })
</script>
若用 toRefs 解构出 ref,模板里也可以写 count、name:
const { count, name } = toRefs(state)
<template>
<p>{{ count }} {{ name }}</p>
</template>
十、区别六:传给函数或 composable
传 ref:函数里要 .value
const count = ref(0)
function add(n) {
n.value++
}
add(count)
传 reactive:函数里直接改属性
const state = reactive({ count: 0 })
function add(obj) {
obj.count++
}
add(state)
在组合式函数里,若希望“返回一组可写的响应式数据”,通常返回 ref(或 toRefs 出来的 ref),这样对方用 .value 或自动解包即可,不会误把整个 reactive 替换掉。
十一、用 ref 包对象 vs 用 reactive 包对象
相同点
两种方式都能让对象“响应式”,改属性都会更新视图:
const userRef = ref({ name: 'a', age: 18 })
const userReactive = reactive({ name: 'a', age: 18 })
userRef.value.name = 'b'
userReactive.name = 'b'
不同点
-
ref 包对象:
- script 里要 userRef.value.xxx。
- 可以 userRef.value = { … } 整体替换。
- 解构一般不解构“整个对象”,而是需要时用 toRefs(userRef.value) 或自己包 ref。
-
reactive 包对象:
- script 里 userReactive.xxx,没有 .value。
- 不能 userReactive = { … } 整体替换。
- 解构会丢响应式,需 toRefs(userReactive) 再解构。
若你经常要整体替换这个对象(例如每次请求用户信息都整份覆盖),用 ref 更合适;若只是一直改属性、不替换,用 reactive 或 ref 都可以,看团队习惯。
十二、toRefs:reactive 解构的“救星”
toRefs(reactive 对象) 会把对象里每个属性转成 ref,这样解构出来的每个属性仍是响应式的:
import { reactive, toRefs } from 'vue'
const state = reactive({
count: 0,
name: ''
})
const { count, name } = toRefs(state)
count.value++
name.value = 'hi'
- 模板里可以写 count、name。
- count.value 和 state.count 是同一份数据,改谁都会更新。
适合:想用 reactive 管理“一组字段”,又想在模板或函数里按名字用(而不是一直写 state.xxx)时。
十三、常见坑与注意点
13.1 reactive 整体替换
const state = reactive({ count: 0 })
state = reactive({ count: 1 }) // 错误,响应式断了
应只改属性,或改用 ref 包对象再整体替换。
13.2 reactive 解构不包 toRefs
const { count } = reactive({ count: 0 })
count++ // 不生效
应 toRefs 后再解构,或不用解构,一直写 state.count。
13.3 ref 在 script 里忘记 .value
const count = ref(0)
count++ // 错误:count 是 ref 对象,这样改的不是数值
count.value++
13.4 把 reactive 赋给 ref 再整体替换
若你有一个 reactive,后来想“整份换掉”,不能直接 state = 新对象。可以一开始就用 ref 包对象,或把新对象的属性逐个赋给 state(Object.assign(state, 新对象)),保留同一引用。
十四、怎么选?简单决策
- 单值(一个数字、字符串、布尔、一个“我要整体替换”的对象或数组)→ 用 ref。
- 一组强相关的字段(表单 state、页面 state),且不会整份替换、希望少写 .value → 用 reactive;若需要解构或传出去用,配合 toRefs。
- 既要“一组字段”又要“有时整份替换”(例如先 reactive 一坨,后来接口返回整份覆盖)→ 用 ref 包对象,或一开始就 ref({ … })。
十五、速查表
| 需求 | 用谁 | 说明 |
|---|---|---|
| 一个数字/字符串/布尔 | ref | script 里 .value |
| 一个对象,可能要整体替换 | ref | xxx.value = 新对象 |
| 一个对象,只改属性、不替换 | reactive 或 ref 都可 | reactive 无 .value |
| 想解构出一组字段仍响应式 | reactive + toRefs | 解构出来是 ref |
| 模板里少写 state. 前缀 | reactive + toRefs 解构 | 模板写 count、name |
| 组合式函数返回值 | 一般返回 ref(或 toRefs) | 便于对方 .value 或解包 |
十六、学习建议
- 先记牢:ref 要 .value(script),reactive 不能整体替换、reactive 解构要用 toRefs。
- 不确定时,单值或“可能整份换”的对象/数组”用 ref,一般不会错。
- 多写几个对比示例(ref 包对象 vs reactive,解构 vs toRefs),在项目里试一遍,体会会更深。
把本文档里的示例在项目里敲一遍、故意写错几次(如 reactive 整体赋值、解构不加 toRefs),再按表格纠正,会掌握得更牢。祝你学习顺利。