Vue 3 响应式三剑客:ref、shallowRef、reactive,你真的用对了吗?

前言

"用 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?

  1. ✅ 类型统一 --- 所有响应式值都用 .value 访问,不用区分「是否对象」
  2. ✅ 可解构不丢失响应 --- computed、props、ref 都能保持响应
  3. ✅ 组合函数返回值统一 --- 不用猜测返回的是 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 手动处理
}

总结

  1. 默认用 ref --- 通用性最强,心智负担最小 👍
  2. 大数据场景用 shallowRef --- 性能直接翻几十倍 🚀
  3. reactive 不是不能用,但要注意解构陷阱和重新赋值问题
  4. 选择的核心逻辑:不是「对象用 reactive,基本类型用 ref」,而是「是否需要深层响应式追踪」

最后送大家一个口诀:

基本类型就 ref,对象首选还是 ref。 数据量大 shallowRef,整体替换最舒适。 reactive 也可以,只要小心解构陷阱。


参考


如果这篇文章对你有帮助,请点赞 👍 收藏 ⭐ 关注 🔔,我们下期见!

欢迎在评论区留言讨论你的项目中是如何选择 ref / shallowRef / reactive 的。

相关推荐
2603_965896622 小时前
Grid 网格布局|CSS 二维页面排版体系
前端·css
有味道的男人3 小时前
搜好货平台关键词商品搜索接口:分页容错与商品数据轻量化提取方案
服务器·前端·javascript
lovingsoft3 小时前
Chrome Lighthouse 详解
前端·chrome
Qlly3 小时前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝4 小时前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦4 小时前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡4 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI4 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95274 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人4 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring