Vue 3 响应式基础:ref、reactive、toRef 与 toRefs
Vue 3 响应式基础:ref、reactive、toRef 与 toRefs
系列导航:Vue 3 现代开发指南
上一篇:Composition API 与<script setup>
下一篇:派生状态与副作用
Vue 3 使用 Proxy 实现对象响应式,并通过 ref 为任意值提供统一的响应式容器。实际项目中,最稳妥的默认策略是:优先用 ref 表达可能被整体替换的状态,使用 reactive 表达边界清晰、主要按属性修改的对象。
一、ref:统一的响应式容器
ref() 可以接收基本类型或对象:
import { ref } from 'vue'
const count = ref(0)
const title = ref('Vue 3')
const user = ref({ id: 1, name: 'Ada' })
脚本中通过 .value 读写:
count.value++
title.value = 'Vue 3.5'
user.value.name = 'Grace'
user.value = { id: 2, name: 'Lin' }
模板会自动解包顶层 ref:
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button type="button" @click="count++">{{ count }}</button>
</template>
变量本身通常声明为 const。const 限制的是 count 不能指向另一个 ref,不妨碍修改 count.value。
二、reactive:对象的深层响应式代理
reactive() 只接受对象类型,返回原对象的 Proxy:
import { reactive } from 'vue'
const form = reactive({
profile: { name: '', city: '' },
tags: [] as string[],
})
form.profile.name = 'Ada'
form.tags.push('vue')
默认情况下,嵌套对象也会在访问时转为响应式代理。基本类型应使用 ref,不要写 reactive(0)。
三、为什么不能直接替换 reactive 变量
响应式连接存在于 Proxy 上。把变量改指向普通对象,会失去原代理:
let form = reactive({ name: '', city: '' })
// 不推荐:新对象没有沿用模板正在追踪的原代理
form = { name: 'Ada', city: 'London' }
如果对象经常整体替换,用 ref:
const form = ref({ name: '', city: '' })
form.value = { name: 'Ada', city: 'London' }
如果希望保留同一个代理,更新已有属性:
const form = reactive({ name: '', city: '' })
Object.assign(form, { name: 'Ada', city: 'London' })
Object.assign() 不会自动删除源对象里不存在的旧字段,因此它不等价于严格的对象替换。
四、如何选择 ref 和 reactive
| 场景 | 推荐 |
|---|---|
| 字符串、数字、布尔值 | ref |
| 数据可能整体替换 | ref |
| 异步请求结果 | ref |
| 表单对象主要按字段修改 | reactive 或 ref 均可 |
| 希望保持统一心智模型 | 优先 ref |
“对象层级深就一定用 reactive”不是必要规则。对象放进普通 ref 后,内部同样会转为深层响应式。
五、解构为什么会丢失响应式
直接解构 reactive 对象的基本类型属性,只会得到当时的普通值:
const state = reactive({ count: 0, name: 'Ada' })
const { count } = state
state.count++
console.log(count) // 仍然是 0
需要独立引用属性时,使用 toRef():
import { reactive, toRef } from 'vue'
const state = reactive({ count: 0, name: 'Ada' })
const count = toRef(state, 'count')
count.value++
console.log(state.count) // 1
批量转换现有属性时,使用 toRefs():
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Ada' })
const { count, name } = toRefs(state)
count.value++
name.value = 'Grace'
toRefs() 只处理调用时对象上可枚举的属性。对于当时不存在、之后才加入的属性,应直接使用 toRef(object, key)。
六、Props 解构是一个特殊情况
Vue 3.5 中,编译器会让 defineProps() 的解构变量保持响应式:
<script setup lang="ts">
interface Props {
page?: number
pageSize?: number
}
const { page = 1, pageSize = 20 } = defineProps<Props>()
</script>
这段代码不同于普通 reactive 对象解构,是 <script setup> 编译器提供的能力。不要据此推断所有响应式对象都可以直接解构。
如果要把解构后的 prop 传给 watch,传 getter:
watch(() => page, (newPage) => {
console.log(newPage)
})
直接 watch(page, ...) 传入的是当前数值,不是可监听的数据源。
七、ref 的自动解包边界
模板中的顶层 ref 会自动解包,但数组、Map 等原生集合里的 ref 不会自动解包:
const books = reactive([ref('Vue Guide')])
<template>
<p>{{ books[0].value }}</p>
</template>
作为深层响应式普通对象属性的 ref 会被解包并与原 ref 保持连接:
const count = ref(1)
const state = reactive({ count })
console.log(state.count) // 1
state.count = 2
console.log(count.value) // 2
不要为了省略 .value 而刻意把 ref 塞进复杂对象。
八、TypeScript 类型写法
通常让 TypeScript 从初始值推断类型即可:
const count = ref(0) // Ref<number>
const user = reactive({ id: 1, name: 'Ada' })
值可能为空时显式声明联合类型:
interface User {
id: number
name: string
}
const selectedUser = ref<User | null>(null)
不要用不真实的类型断言掩盖运行时空值。
九、一个实际表单示例
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
interface ProfileForm {
name: string
email: string
}
const form = reactive<ProfileForm>({ name: '', email: '' })
const submitting = ref(false)
const isValid = computed(
() => form.name.trim().length > 0 && form.email.includes('@'),
)
async function submit() {
if (!isValid.value || submitting.value) return
submitting.value = true
try {
await saveProfile({ ...form })
} finally {
submitting.value = false
}
}
async function saveProfile(payload: ProfileForm) {
console.log(payload)
}
</script>
<template>
<form @submit.prevent="submit">
<input v-model.trim="form.name" placeholder="姓名" />
<input v-model.trim="form.email" type="email" placeholder="邮箱" />
<button :disabled="!isValid || submitting">
{{ submitting ? '提交中…' : '保存' }}
</button>
</form>
</template>
表单字段适合放在 reactive 对象里,提交状态则是独立的 ref。两种 API 可以自然组合,不需要选边站。
小结
ref 能表达所有类型并支持整体替换,是很好的默认选择;reactive 适合围绕同一对象逐字段修改。普通响应式对象直接解构会断开基本类型属性的响应式连接,需要 toRef() 或 toRefs();Vue 3.5 的 Props 响应式解构属于编译器特例。