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

前言

"用 ref 还是 reactive?shallowRef 又是什么鬼?"

Vue 3 的 Composition API 带来了 refreactiveshallowRef 三套响应式 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. 可解构不丢失响应 --- computedpropsref 都能保持响应
  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 的。

相关推荐
触底反弹1 小时前
🔥 JavaScript this 指向全解析:面试必考的 5 大绑定规则
前端·javascript·面试
Csvn2 小时前
TypeScript 类型守卫的 4 个实战级别:从 `filter(Boolean)` 到自定义类型谓词
前端
骑士雄师2 小时前
langchain:推荐切片策略
服务器·前端·langchain
云浪2 小时前
拆开 WAV,看见声音——JavaScript 二进制数组实战全解析
前端·javascript·ecmascript 6
_瑞3 小时前
深入理解灵动岛
前端·ios·app
想你依然心痛3 小时前
嵌入式日志系统:分级日志、环形缓冲区与远程输出——运行时调试、非侵入
android·前端·javascript
这是个栗子3 小时前
【Vue代码分析】 import.meta.env.DEV:现代前端工程中的环境变量
前端·javascript·vue.js·环境变量
像牛奶却不是牛奶3 小时前
记录几个前端实用小函数
前端·javascript
IDIOT___IDIOT3 小时前
Windows 解锁后 Chrome 弹出白屏的排查与修复
前端·chrome