前言
"用 ref 还是 reactive?shallowRef 又是什么鬼?"
Vue 3 的 Composition API 带来了 ref、reactive 和 shallowRef 三套响应式 API。很多新手甚至老手在使用时都会纠结:到底该用哪个?
有人说「全用 ref」,有人说「对象用 reactive,简单值用 ref」------这些建议对吗?它们各自有什么坑?什么时候必须用 shallowRef?
今天一次性讲清楚。
1️⃣ 核心概念速览
先给个全局印象:
| API | 监听深度 | 适用场景 | 性能 |
|---|---|---|---|
ref |
深层 | 任何值(推荐默认选择) | 中等 |
reactive |
深层 | 纯对象 / 数组 | 中等 |
shallowRef |
浅层 | 大数据 / 第三方实例 / 只读对象 | 🚀 最快 |
一句话总结:
默认用 ref,对象嵌套深且需要响应式追踪用 reactive,数据量大或不需要内部响应时用 shallowRef。
2️⃣ ref --- 全能选手
基本用法
typescript
import { ref } from 'vue'
// 基础值
const count = ref(0)
count.value++ // ✅ 响应式
// 对象
const user = ref({ name: '张三', info: { age: 25 } })
user.value.name = '李四' // ✅ 触发更新
user.value.info.age = 26 // ✅ 也触发更新
原理揭秘
ref() 的本质是:
typescript
// 近似实现
function ref(value) {
// 如果是对象,用 reactive 包裹
if (isObject(value)) {
return { value: reactive(value) }
}
// 基本类型,用 getter/setter
return new RefImpl(value)
}
也就是说,ref 传对象 = reactive 套了一层 .value。
为什么推荐默认用 ref?
- ✅ 类型统一 --- 所有响应式值都用
.value访问,不用区分「是否对象」 - ✅ 可解构不丢失响应 ---
computed、props、ref都能保持响应 - ✅ 组合函数返回值统一 --- 不用猜测返回的是 ref 还是 reactive
typescript
// 组合函数
function useCounter() {
const count = ref(0)
const double = computed(() => count.value * 2)
return { count, double } // 解构后依然响应式 ✅
}
const { count, double } = useCounter()
使用 ref 的注意事项
❌ ref 的 .value 在模板中自动解包,但只在顶层属性才生效:
vue
<template>
{{ count }} <!-- ✅ 正确,自动解包 -->
{{ user.name }} <!-- ✅ 正确 -->
{{ obj.nested.count }} <!-- ❌ 不会自动解包! -->
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const user = ref({ name: '张三' })
const obj = { nested: { count: ref(0) } }
// 要用 obj.nested.count.value 才行
</script>
3️⃣ reactive --- 对象专属
基本用法
typescript
import { reactive } from 'vue'
const state = reactive({
name: '张三',
info: {
age: 25,
tags: ['前端', 'Vue']
}
})
state.name = '李四' // ✅ 触发更新
state.info.age = 26 // ✅ 深层响应
state.info.tags.push('React') // ✅ 数组变化也响应
reactive 的陷阱 🚨
陷阱 1:不能直接赋值整个对象
typescript
let state = reactive({ count: 0 })
// ❌ 错误:直接赋值会断开响应式连接
state = { count: 1 }
// ✅ 正确:修改属性
state.count = 1
// ✅ 正确:用 Object.assign
Object.assign(state, { count: 1 })
陷阱 2:解构会丢失响应
typescript
const state = reactive({ count: 0, name: '张三' })
// ❌ 解构后 count 和 name 不再是响应式
const { count, name } = state
// ✅ 保持响应式需要用 toRefs
import { toRefs } from 'vue'
const { count, name } = toRefs(state)
// 现在 count 是 ref,count.value 访问
陷阱 3:不能用于基本类型
typescript
// ❌ reactive 不支持基本类型!
reactive(0) // 警告
reactive('hello') // 警告
// ✅ 用 ref
ref(0)
ref('hello')
什么时候用 reactive?
- ✅ 纯对象(从 API 返回的大对象)
- ✅ 需要深层嵌套的响应式追踪
- ✅ 团队约定所有响应式对象都用 reactive
但说实话,现在推荐全用 ref 的场景越来越多了,包括 Vue 官方文档也在倾向 ref。
4️⃣ shallowRef --- 性能之王
基本用法
typescript
import { shallowRef, triggerRef } from 'vue'
const state = shallowRef({ count: 0, info: { name: '张三' } })
// ✅ 触发更新:替换整个 .value
state.value = { count: 1, info: { name: '李四' } }
// ❌ 不触发更新:修改内部属性
state.value.count = 2 // 视图不会更新!
state.value.info.name = '王五' // 视图不会更新!
// ✅ 强制触发更新
triggerRef(state) // 手动触发 shallowRef 的更新
原理对比
来看响应式追踪的差异:
typescript
// ref --- 内部属性也被追踪
const normal = ref({ data: [1,2,3], meta: { page: 1 } })
// 访问 normal.value.data[0] → 触发依赖收集 ✅
// 修改 normal.value.meta.page = 2 → 触发更新 ✅
// shallowRef --- 只追踪 .value 的「替换」
const shallow = shallowRef({ data: [1,2,3], meta: { page: 1 } })
// 访问 shallow.value.data[0] → 不收集 ✅(不追踪内部)
// 修改 shallow.value.meta.page = 2 → 不触发更新 ✅(没追踪)
// shallow.value = { ... } → 触发更新 ✅
看这个对比就明白了:shallowRef 只监听「指针」变化,内部数据变化它不管。
性能测试
用 10000 条数据做实验:
typescript
// 10000 条数据的数组
const bigData = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
nested: { value: Math.random() }
}))
// ref 版本:每个对象、每个属性都变成响应式
const dataRef = ref(bigData)
// 耗时:约 15ms 👎
// shallowRef 版本:只做一层代理
const dataShallow = shallowRef(bigData)
// 耗时:约 0.5ms 👍(快了 30 倍!)
shallowRef 的最佳实践
场景 1:大数据表格
typescript
const tableData = shallowRef([])
// 从后端获取数据
async function fetchData() {
const res = await api.getUsers()
// ✅ 直接替换整个数组,触发更新
tableData.value = res.data
}
// 修改单行时,替换整个数组
function updateRow(id, newData) {
tableData.value = tableData.value.map(
item => item.id === id ? { ...item, ...newData } : item
)
}
场景 2:第三方非响应式实例
typescript
// 当组件接受一个只做「渲染」但不需内部响应的大对象
import * as echarts from 'echarts'
const chartOptions = shallowRef({
series: [
// 大量复杂的配置数据...
]
})
// 更新时整个替换
chartOptions.value = getNewOptions()
场景 3:使用 JSON 序列化的数据
typescript
// 从 localStorage 读取大 JSON
const cache = shallowRef(JSON.parse(localStorage.getItem('bigCache') || '{}'))
function saveCache(newCache: Record<string, any>) {
cache.value = newCache // 整体替换
localStorage.setItem('bigCache', JSON.stringify(newCache))
}
5️⃣ 进阶对比:自定义 shallowReactive
Vue 3.3+ 其实还暴露了一些底层 API,但 shallowRef + triggerRef 的组合已经覆盖大部分场景。
如果你既想要「浅层响应」又不想用 .value:
typescript
import { shallowReactive } from 'vue'
const state = shallowReactive({
count: 0,
user: { name: '张三' }
})
state.count++ // ✅ 第一层触发更新
state.user.name = '李四' // ❌ 深层不触发!和 shallowRef 一个道理
其实就是 reactive 的浅层版本,和 shallowRef 的关系类似。
| ref 系列 | reactive 系列 | |
|---|---|---|
| 深层 | ref() |
reactive() |
| 浅层 | shallowRef() |
shallowReactive() |
| 只读 | readonly(ref()) |
readonly(reactive()) |
6️⃣ 实战选择指南
决策流程图
csharp
要响应式吗?
├─ 否 → 普通变量就够了
└─ 是 →
├─ 基本类型(number/string/bool)→ ref
└─ 对象/数组 →
├─ 数据量巨大 (>1000条) → shallowRef + 整体替换
├─ 第三方非响应式对象 → shallowRef
├─ 需要深层响应追踪 →
│ ├─ 团队习惯 → reactive 或 ref 都行
│ └─ 我个人推荐 → ref(更安全)
└─ 其他情况 → ref
快速决策表
typescript
// ✅ 基本类型 → ref
const count = ref(0)
const name = ref('张三')
// ✅ 表单对象 → ref(推荐)或 reactive
const form = ref({ name: '', age: 0 })
// 或
const form = reactive({ name: '', age: 0 })
// ✅ 列表数据 → ref 或 shallowRef(大数据用)
const list = ref(['a', 'b', 'c'])
const bigList = shallowRef([])
// ✅ 从 API 获取的深层对象 → ref
const article = ref({
title: '',
content: '',
author: { name: '', avatar: '' }
})
// ✅ 不需要内部响应的大对象 → shallowRef
const config = shallowRef({
rules: [/* 上百条规则 */],
mappings: { /* 大量映射 */ }
})
7️⃣ 源码级理解
想深入理解的朋友,看这一节就够了。
Vue 3 的响应式核心依赖 Proxy:
typescript
// 伪代码:reactive vs shallowReactive 的区别
function reactive(target) {
// 深层代理:访问任意属性都返回 reactive 包裹的值
return new Proxy(target, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
// 🔑 关键:递归代理!访问属性时继续包装
if (isObject(value)) {
return reactive(value)
}
return value
}
})
}
function shallowReactive(target) {
// 浅层代理:只代理第一层
return new Proxy(target, {
get(target, key, receiver) {
// 🔑 不递归!直接返回原始值
return Reflect.get(target, key, receiver)
}
})
}
而 ref 的核心逻辑:
typescript
class RefImpl {
private _value: any
private _rawValue: any
constructor(value) {
this._rawValue = value
// 如果是对象交给 reactive 处理
this._value = toReactive(value)
}
get value() {
// 收集依赖
track(this, 'value')
return this._value
}
set value(newVal) {
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = toReactive(newVal) // 新值是对象也会 reactive
// 触发更新
trigger(this, 'value')
}
}
}
function shallowRef(value) {
// 区别就在这里:不用 toReactive,直接存原始值
return new ShallowRefImpl(value)
}
class ShallowRefImpl {
private _value: any
constructor(value) {
this._value = value // 不做 reactive 包装!
}
get value() {
track(this, 'value')
return this._value
}
set value(newVal) {
if (hasChanged(newVal, this._value)) {
this._value = newVal // 直接赋值,不包装
trigger(this, 'value')
}
}
}
一句话:ref = shallowRef + toReactive 包装,而 toReactive = reactive。没了。
8️⃣ 高级技巧
技巧一:shallowRef + triggerRef 精确控制
当你用 shallowRef 但偶尔需要手动触发更新时:
typescript
import { shallowRef, triggerRef } from 'vue'
const state = shallowRef({ count: 0 })
function increment() {
state.value.count++
triggerRef(state) // 手动通知更新
}
技巧二:ref 和 shallowRef 混合使用
typescript
// 总体用 shallowRef 保证性能
const bigData = shallowRef<BigItem[]>([])
// 但某个子属性需要响应式
import { ref } from 'vue'
const selectedIndex = ref(-1)
function selectItem(index: number) {
selectedIndex.value = index
// bigData 不需要变,选中的高亮状态由 selectedIndex 控制
}
技巧三:深层 ref 的性能优化
typescript
// ❌ 每次访问都深度代理
const data = ref(hugeObject)
// ✅ 用 shallowRef + 按需 reactive
const data = shallowRef(hugeObject)
// 只有需要响应式的部分才手动包装
if (needReactive) {
// 用 computed 或者 watch 手动处理
}
总结
- 默认用
ref--- 通用性最强,心智负担最小 👍 - 大数据场景用
shallowRef--- 性能直接翻几十倍 🚀 reactive不是不能用,但要注意解构陷阱和重新赋值问题- 选择的核心逻辑:不是「对象用 reactive,基本类型用 ref」,而是「是否需要深层响应式追踪」
最后送大家一个口诀:
基本类型就 ref,对象首选还是 ref。 数据量大 shallowRef,整体替换最舒适。 reactive 也可以,只要小心解构陷阱。
参考
如果这篇文章对你有帮助,请点赞 👍 收藏 ⭐ 关注 🔔,我们下期见!
欢迎在评论区留言讨论你的项目中是如何选择 ref / shallowRef / reactive 的。