4、响应式核心:ref 和 reactive 的区别、toRefs 的使用场景、shallowRef 与 shallowReactive
响应式系统,说白了就是 Vue 的灵魂。我刚开始用 Vue 3 的时候,最困惑的就是 ref 和 reactive 到底该用哪个。今天咱们就把这几个核心概念彻底捋清楚。
4.1 ref 和 reactive:一对欢喜冤家
先说说它们最直观的区别。ref 可以包裹基本类型和对象,而 reactive 只能处理对象。你可能会问:「那 ref 包裹对象时,内部是怎么处理的?」其实 ref 内部也是调用了 reactive。
核心区别速览:
- ref:可以处理基本类型(number、string、boolean 等)和对象
- reactive:只能处理对象(包括数组、Map、Set 等)
- ref:访问时需要用 .value,模板中自动解包
- reactive:直接访问属性,不需要 .value
// ref 示例
import { ref } from 'vue'
const count = ref(0)
const user = ref({ name: '张三', age: 25 })
console.log(count.value) // 0
console.log(user.value.name) // 张三
// reactive 示例
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: { name: '张三', age: 25 }
})
console.log(state.count) // 0
console.log(state.user.name) // 张三
我个人习惯是:基本类型用 ref,复杂对象用 reactive。但这不是绝对的。我在项目中遇到过一个问题:用 reactive 包裹一个从接口返回的数据,结果不小心把整个对象重新赋值了,响应式就丢了。
注意:reactive 不能直接替换整个对象!如果你需要重新赋值,要么用 ref,要么用 Object.assign。
// 错误做法
let state = reactive({ count: 0 })
state = { count: 1 } // 响应式丢失!
// 正确做法
const state = ref({ count: 0 })
state.value = { count: 1 } // 没问题
// 或者用 Object.assign
const state = reactive({ count: 0 })
Object.assign(state, { count: 1 }) // 保持响应式
4.2 toRefs:解构不失响应式
你想想看,有时候我们想从 reactive 对象里解构出几个属性单独用,但直接解构会丢失响应式。这时候 toRefs 就派上用场了。
import { reactive, toRefs } from 'vue'
const user = reactive({
name: '李四',
age: 30,
email: 'lisi@example.com'
})
// 直接解构 - 响应式丢失
const { name, age } = user
name.value = '王五' // 不会触发更新
// 使用 toRefs - 保持响应式
const { name, age } = toRefs(user)
name.value = '王五' // 会触发更新
toRefs 的使用场景其实很明确:当你需要把 reactive 对象的属性暴露出去,或者需要在模板中单独使用某个属性时。我在项目中经常用它来拆分大型的状态对象,让代码更清晰。
小技巧:toRefs 返回的是一个普通对象,每个属性都是 ref。所以访问时要用 .value,但在模板中会自动解包。
4.3 shallowRef 与 shallowReactive:性能优化的利器
为什么要用浅层响应式?说白了,就是性能优化。默认的 ref 和 reactive 是深层响应式的,会递归地给所有嵌套属性添加响应式。如果数据结构很深,或者数据量很大,这就会带来性能开销。
import { shallowRef, shallowReactive } from 'vue'
// shallowRef:只追踪 .value 的变化
const data = shallowRef({
list: [1, 2, 3],
info: { name: '测试' }
})
data.value = { list: [4, 5, 6] } // 触发更新
data.value.list.push(7) // 不会触发更新!
// shallowReactive:只追踪第一层属性的变化
const state = shallowReactive({
user: { name: '赵六' },
items: [1, 2, 3]
})
state.user = { name: '钱七' } // 触发更新
state.user.name = '孙八' // 不会触发更新!
我曾经在做一个大型表格组件时,数据有几千行,每行还有嵌套对象。用 reactive 包裹后,页面卡得不行。后来换成 shallowReactive,只监听第一层的变化,性能直接翻倍。
什么时候用浅层响应式?
- 数据结构很深,但你只关心顶层变化
- 数据量很大,深层响应式导致性能问题
- 配合 immutable 数据使用(比如每次更新都生成新对象)
4.4 实战选择指南
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 基本类型(数字、字符串等) | ref | reactive 不支持基本类型 |
| 简单对象(2-3层嵌套) | reactive | 访问方便,不需要 .value |
| 需要解构的对象 | reactive + toRefs | 保持响应式不丢失 |
| 大型数据(深层嵌套) | shallowRef / shallowReactive | 性能优化 |
| 需要整体替换的数据 | ref | reactive 不能直接替换 |
避坑指南:我曾经在项目中用 shallowRef 包裹了一个数组,然后直接修改数组元素,结果视图没更新。后来才意识到,shallowRef 只监听 .value 的变化,数组内部元素的修改是不会触发的。如果你需要修改数组内部,要么用普通 ref,要么每次生成新数组赋值给 .value。
嗯,到这里你应该对这几个响应式 API 有了清晰的认识。记住一个原则:默认用 ref 和 reactive,遇到性能问题再考虑浅层版本。这样既保证了代码的可维护性,又能在需要时进行优化。