第 3 章:响应式 ref / reactive
学习目标
- 区分
ref与reactive的使用场景 - 用
toRefs解构不丢失响应性 - 理解
shallowRef和readonly的边界 - 避开响应式丢失的 4 个坑
一、ref:基本类型首选
vue
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0) // 基本类型
const msg = ref('hello')
count.value++ // 脚本里要 .value
console.log(count.value) // 1
</script>
<template>
<p>{{ count }}</p> <!-- 模板里自动解包,不用 .value -->
</template>二、reactive:对象类型
vue
<script setup lang="ts">
import { reactive } from 'vue'
const user = reactive({
name: 'Alice',
age: 18,
addr: { city: '上海' },
})
user.age = 19 // ⚠️ reactive 不需要 .value
user.addr.city = '北京' // 深层响应
</script>| 维度 | ref(0) | reactive({}) |
|---|---|---|
| 适用 | 基本 / 对象都行 | 仅对象 |
| 访问 | .value | 直接 obj.key |
| 解构 | 丢失响应性 | 丢失响应性 |
| 替换 | .value = {} 可整体替换 | ❌ 不能再赋值 |
⚠️ 坑 1:千万别给
reactive整体赋值user = { ... },会丢失响应。改用Object.assign(user, { ... })。
三、解构保留响应性
ts
import { toRefs, toRef } from 'vue'
const user = reactive({ name: 'A', age: 18 })
const { name, age } = toRefs(user) // 解构后仍是 Ref
const age2 = toRef(user, 'age') // 单独拎一个字段setup 返回的对象传到模板时,Vue 会自动解包 ref,所以模板里直接 {{ name }} 即可。
💡
toRefs()创建的 ref 跟原 reactive 对象是双向绑定——改name.value等于改user.name,反过来也一样:tsname.value = 'B' console.log(name.value) // 'B' console.log(user.name) // 'B' ← user 跟着变了 user.name = 'C' console.log(name.value) // 'C' ← name 也跟着变了底层是 getter/setter,两边读写都走同一个数据源,不是"复制"。所以
toRefs是"指针"不是"快照"。
四、shallowRef 只追踪 .value 变化
ts
import { shallowRef } from 'vue'
const state = shallowRef({ count: 0 })
state.value.count++ // ❌ 不会触发更新
state.value = { count: 1 } // ✅ 替换引用才会更新使用场景:大对象、第三方实例、只需整体替换时。
五、readonly 防止误改
ts
import { readonly } from 'vue'
const origin = reactive({ name: 'A' })
const copy = readonly(origin) // 深只读
copy.name = 'B' // ⚠️ 开发环境会报警告,生产静默失败六、ref 与 reactive 互转
ts
import { ref, unref, isRef } from 'vue'
const r = ref(0)
unref(r) // 0,等价于 isRef(r) ? r.value : r
isRef(r) // true七、实战:表单对象
vue
<script setup lang="ts">
import { reactive, ref } from 'vue'
const form = reactive({
username: '',
password: '',
})
const loading = ref(false)
const submit = async () => {
loading.value = true
try {
await api.login(form) // 直接传 reactive 对象
} finally {
loading.value = false
}
}
</script>⚠️ 坑 2:
reactive不能直接await reactive(...),也不要把它放进Map/Set当 key。 ⚠️ 坑 3:ref({})在模板里访问子字段会自动解包({{ obj.a }}不用.value),但对象整体仍要.value。 ⚠️ 坑 4:Pinia store 里优先用ref不用reactive,避免 SSR 序列化问题。
八、本章小结
| API | 何时用 |
|---|---|
ref | 基本类型、需要 .value 语义 |
reactive | 复杂对象,内部全用 obj.key |
toRefs / toRef | 解构保留响应性 |
shallowRef | 大数据 / 整体替换 |
readonly | 防止组件 props 被改 |
动手练习
- 计数器:用
ref写一个 + / - / 重置按钮 - 表单:用
reactive写登录表单{ username, password },提交时打印 - 解构:把表单字段用
toRefs拆出来,绑定到 input 上,确认还能响应
下一章:第 4 章:computed / watch →