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,遇到性能问题再考虑浅层版本。这样既保证了代码的可维护性,又能在需要时进行优化。