Vue 3 响应式双雄:ref 与 reactive 完全指南

前言

在 Vue 3 的 Composition API 中,refreactive 是最核心的两个响应式 API。几乎每个 Vue 3 组件都离不开它们。然而,很多开发者在实际使用中仍然困惑:

  • refreactive 到底有什么区别?
  • 为什么 ref.value,而 reactive 不用?
  • 到底什么时候用 ref,什么时候用 reactive
  • 为什么解构 reactive 会丢失响应性?

今天这篇文章,我将从为什么需要它们 开始,到使用方式底层原理如何选择 ,再到注意事项 ,带你一次性彻底搞懂 refreactive

一、为什么需要 ref 和 reactive?

在 Vue 3 之前,Vue 2 的响应式系统基于 Object.defineProperty 实现。这种方式有两个明显的局限:

  1. 无法监听对象新增/删除属性 :必须使用 Vue.set / Vue.delete
  2. 无法直接监听数组索引修改arr[0] = x 不会触发更新

更重要的是,在 Composition API 中,我们需要一种方式让普通的 JavaScript 变量变成响应式的。当数据变化时,依赖这些数据的 UI 能够自动更新。

refreactive 就是为此而生的两个核心 API。它们都能让数据变得"响应式",但设计哲学和使用方式截然不同。

二、ref:值级别的响应式容器

2.1 什么是 ref?

ref 用于创建一个响应式的数据引用 。它本质上是一个包装对象 ------内部包含一个 .value 属性,通过 getter/setter 实现对 .value 的读写拦截。

javascript 复制代码
import { ref } from 'vue'

const count = ref(0)
// 实际结构类似于:{ value: 0 }

console.log(count.value) // 0
count.value++            // 修改值

2.2 ref 的核心特点

特点 说明
适用类型 任意类型(基本类型、对象、数组均可)
访问方式 必须通过 .value 访问和修改
模板中使用 自动解包,无需 .value
重新赋值 ✅ 可以安全地重新赋值,保持响应性
解构 ❌ 解构会失去响应性

2.3 ref 的底层原理

ref 的实现相对直观------它创建了一个带有 .value 属性的对象,通过 getter/setter 来追踪依赖和触发更新:

javascript 复制代码
// 简化版的 ref 实现
function ref(value) {
  // 如果传入的是对象,内部会用 reactive 包裹
  if (isObject(value)) {
    value = reactive(value)
  }
  
  return {
    get value() {
      track()   // 收集依赖
      return value
    },
    set value(newVal) {
      value = newVal
      trigger() // 触发更新
    }
  }
}

关键点在于:

  • 当包装的是对象 时,ref 内部会使用 reactive 来确保对象的响应性
  • 基本类型通过 .value 的 getter/setter 实现响应式
  • 模板中会自动解包 ref,所以不用写 .value

2.4 ref 的使用示例

vue 复制代码
<script setup>
import { ref } from 'vue'

// 基本类型
const count = ref(0)
const message = ref('Hello')
const isLoading = ref(false)

// 对象类型(ref 也可以包裹对象)
const user = ref({ name: '张三', age: 25 })

// 修改基本类型
count.value++
message.value = 'World'

// 修改对象(需要 .value)
user.value.name = '李四'

// 重新赋值整个对象------✅ 保持响应性
user.value = { name: '王五', age: 30 }
</script>

<template>
  <!-- 模板中自动解包,不需要 .value -->
  <p>{{ count }}</p>
  <p>{{ message }}</p>
  <p>{{ user.name }}</p>
</template>

三、reactive:结构级别的响应式代理

3.1 什么是 reactive?

reactive 用于将对象(包括数组、Map、Set 等) 整体转换为响应式数据。它返回的是原对象的 Proxy 代理,对代理对象的任何属性读写都会被拦截。

javascript 复制代码
import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: { name: '张三', age: 25 }
})

state.count++          // 直接修改,无需 .value
state.user.name = '李四'

3.2 reactive 的核心特点

特点 说明
适用类型 仅对象类型(Object、Array、Map、Set 等)
访问方式 直接访问属性,无需 .value
模板中使用 直接使用,与普通对象无异
重新赋值 ❌ 不能直接重新赋值整个对象,否则失去响应性
解构 ❌ 解构会丢失响应性

3.3 reactive 的底层原理

reactive 基于 ES6 的 Proxy 实现。Proxy 可以拦截对目标对象的所有操作------读取、写入、删除、遍历等:

javascript 复制代码
// 简化版的 reactive 实现
function reactive(target) {
  if (typeof target !== 'object' || target === null) {
    return target
  }
  
  return new Proxy(target, {
    get(obj, key) {
      track(obj, key)        // 收集依赖
      const res = obj[key]
      // 嵌套对象递归代理
      return typeof res === 'object' ? reactive(res) : res
    },
    set(obj, key, value) {
      const result = Reflect.set(obj, key, value)
      trigger(obj, key)      // 触发更新
      return result
    },
    deleteProperty(obj, key) {
      const result = Reflect.deleteProperty(obj, key)
      trigger(obj, key)      // 删除属性也能触发更新
      return result
    }
  })
}

Vue 2 vs Vue 3 响应式对比

能力 Vue 2 (defineProperty) Vue 3 (Proxy)
监听数组索引修改
监听 push/pop ⚠️ 重写方法 ✅ 原生支持
监听新增属性 ❌ 需 Vue.set ✅ 自动监听
监听删除属性 ❌ 需 Vue.delete ✅ 自动监听
性能 初始化遍历所有属性 惰性代理,更高效

3.4 reactive 的使用示例

vue 复制代码
<script setup>
import { reactive } from 'vue'

// 定义响应式对象
const form = reactive({
  username: '',
  email: '',
  password: '',
  age: 0
})

// 定义响应式数组
const todoList = reactive([
  { id: 1, text: '学习 Vue 3', done: false },
  { id: 2, text: '写一篇博客', done: false }
])

// 修改属性------直接操作,无需 .value
form.username = 'andy'
form.age = 25

// 修改数组------直接操作
todoList[0].done = true
todoList.push({ id: 3, text: '分享给朋友', done: false })

// ⚠️ 错误:重新赋值整个对象会丢失响应性
// form = { username: 'new', email: 'new@email.com' }  // ❌ 不要这样做!
</script>

<template>
  <input v-model="form.username" />
  <input v-model="form.email" />
  <ul>
    <li v-for="item in todoList" :key="item.id">
      {{ item.text }}
    </li>
  </ul>
</template>

四、ref vs reactive:全景对比

对比维度 ref reactive
适用类型 任意类型 仅对象(Object/Array/Map/Set)
访问方式 .value 直接访问属性
模板中使用 自动解包,无需 .value 直接使用
重新赋值 ✅ 可以安全重新赋值 ❌ 重新赋值会丢失响应性
解构 ❌ 解构后失去响应性 ❌ 解构后失去响应性
底层实现 包装对象 + .value 的 getter/setter Proxy 代理整个对象
嵌套对象 内部自动调用 reactive 深度代理
响应式级别 值级别 结构级别

五、真实项目实战:如何选择?

5.1 选择策略

根据官方推荐和社区最佳实践:

场景 推荐方案 理由
基本类型(number/string/boolean) ref reactive 不支持基本类型
单个独立值(计数、开关、输入框) ref 结构简单,语义清晰
表单数据(多个关联字段) reactive 属性直接访问,代码简洁
复杂对象/配置项 reactive 保持结构完整性
组合式函数返回值 ref 更灵活,类型统一
需要整体替换的数据 ref reactive 重新赋值会丢失响应性
不确定用哪个 ref 更通用,更灵活

官方倾向 :Vue 官方文档强烈推荐使用 ref() 作为声明响应式状态的主要 API。非必要情况下最好避免使用 reactive。但在实际开发中,两者各有适用场景,应根据具体情况选择。

5.2 实战案例一:计数器 + 用户表单

vue 复制代码
<script setup>
import { ref, reactive } from 'vue'

// ✅ 场景1:基本类型 → 用 ref
const count = ref(0)
const isLoading = ref(false)

const increment = () => {
  count.value++
}

// ✅ 场景2:表单数据(关联性强)→ 用 reactive
const form = reactive({
  name: '',
  email: '',
  phone: '',
  age: 0
})

const submitForm = () => {
  console.log('提交:', form)
}

// ✅ 场景3:混合使用
const userInfo = reactive({
  id: 1,
  name: '张三',
  // 独立的状态用 ref
  isEditing: ref(false),
  permissions: ref(['read', 'write'])
})
</script>

5.3 实战案例二:列表管理(数组场景)

对于数组,refreactive 都能用,但行为有差异:

vue 复制代码
<script setup>
import { ref, reactive } from 'vue'

// 方式一:用 ref 包裹数组
const list1 = ref([1, 2, 3])
// 修改:需要 .value
list1.value.push(4)
list1.value = [1, 2, 3, 4, 5]  // ✅ 整体替换也没问题

// 方式二:用 reactive 包裹数组
const list2 = reactive([1, 2, 3])
// 修改:直接操作
list2.push(4)
// ⚠️ 不能整体替换
// list2 = [1, 2, 3, 4, 5]  // ❌ 会丢失响应性
// 正确做法:清空后重新填充
list2.length = 0
list2.push(1, 2, 3, 4, 5)
</script>

建议 :如果数组需要整体替换(如从接口重新获取数据),用 ref 更方便。

六、常见陷阱与注意事项

6.1 ⚠️ 陷阱一:解构 reactive 会丢失响应性

这是 Vue 3 开发中最常见的错误之一:

javascript 复制代码
// ❌ 错误:解构 reactive 对象
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state  // count 和 name 变成了普通变量!
count++  // 不会触发视图更新

解决方案 :使用 toRefs 将每个属性转为 ref

javascript 复制代码
import { reactive, toRefs } from 'vue'

const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state)  // ✅ count 和 name 都是 ref

count.value++  // ✅ 会触发更新

6.2 ⚠️ 陷阱二:给 reactive 重新赋值

javascript 复制代码
// ❌ 错误
let state = reactive({ count: 0 })
state = { count: 1 }  // state 不再是响应式对象!

// ✅ 正确:修改属性,而非替换对象
state.count = 1

// ✅ 正确:如果需要重置,使用 Object.assign
Object.assign(state, { count: 1, name: 'Vue' })

6.3 ⚠️ 陷阱三:在 reactive 中使用基本类型会报警告

javascript 复制代码
// ❌ 错误:reactive 不支持基本类型
const count = reactive(0)  // 控制台警告:value cannot be made reactive

// ✅ 正确:基本类型用 ref
const count = ref(0)

6.4 ⚠️ 陷阱四:ref 在 reactive 数组/集合中不会自动解包

在 Vue 3 中,当 ref 作为响应式数组或原生集合(如 Map)的元素时,不会自动解包 .value,需要手动访问:

javascript 复制代码
const count = ref(0)
const arr = reactive([count])

console.log(arr[0].value)  // 需要 .value,而不是 arr[0]

6.5 ⚠️ 陷阱五:忘记 .value

<script> 中操作 ref 时,必须使用 .value

javascript 复制代码
const count = ref(0)

// ❌ 错误
count++  // 报错:count 是对象,不能自增

// ✅ 正确
count.value++

而在 <template> 中,Vue 会自动解包,不需要 .value

vue 复制代码
<template>
  <p>{{ count }}</p>  <!-- ✅ 不需要 .value -->
</template>

七、总结

维度 ref reactive
核心思想 值级别的响应式容器 结构级别的响应式代理
适用场景 基本类型、需要整体替换的数据、不确定的场景 复杂对象、表单状态、关联性强的数据结构
优点 灵活通用、可安全重新赋值、API 一致 访问无需 .value、代码更简洁、嵌套自动响应
缺点 需要 .value 略显繁琐 不能重新赋值、解构会丢失响应性

最终建议

  1. 新项目优先用 ref:更通用、更灵活、不容易踩坑
  2. 关联性强的对象用 reactive:表单、配置项等场景,代码更简洁
  3. 不确定时用 ref:它几乎可以覆盖所有场景
  4. 牢记 .value :在 <script> 中操作 ref 时不要忘记
  5. 不要解构 reactive :如需解构,使用 toRefs

理解 refreactive 的区别与联系,是掌握 Vue 3 Composition API 的关键一步。希望这篇文章能帮你彻底搞清楚这对"响应式双雄",在实际项目中用得更加得心应手。

相关推荐
随风一样自由9 小时前
【前端独角兽】刚进入前端领域的前端开发用jQuery还是Vue3?
前端·javascript·vue3·jquery
IMPYLH10 小时前
HTML 的 <data> 元素
前端·html
YHHLAI10 小时前
[特殊字符] 面试中的 Promise —— 从入门到精通
前端·javascript·面试
weixin_BYSJ198711 小时前
SpringBoot + MySQL 乒乓球运动员信息管理系统项目实战--附源码04954
java·javascript·spring boot·python·django·flask·php
沪飘大军12 小时前
ll-llm:一个零依赖、可插拔的 OpenAI 兼容 LLM 代理
javascript
leoZ23112 小时前
Claude 驱动的全栈开发:Spring Boot + Vue 的 BS 架构实践
vue.js·spring boot·架构
kyriewen12 小时前
我扒了 GPT-5.6 的全部编程跑分——有一项 OpenAI 没敢放出来
前端·gpt·ai编程
前端H13 小时前
微前端 v3 架构升级:从加载隔离到状态协同
前端·架构·状态模式
山河木马13 小时前
GPU自动处理专题3-NDC怎么映射成屏幕像素坐标(视口变换)
javascript·webgl·图形学