🚀Vue3 ref与reactive深度解析:原理、案例与最佳实践

15 阅读8分钟

在Vue3的Composition API中,refreactive是实现响应式数据的核心API。二者虽均用于构建响应式系统,但适用场景、内部原理与使用方式存在显著差异。本文将从原理层面深度剖析,结合10个实战案例,带你彻底掌握这两个API的用法。

一、核心概念与原理

1. ref:基本类型与引用类型的“通用响应式容器”

  • 定义ref用于创建响应式基本类型(如Number、String、Boolean),也可包裹引用类型(如Object、Array)。
  • 原理:内部通过Object.defineProperty监听value属性的get/set,当包裹引用类型时,会自动将其转换为reactive代理对象。
  • 关键特性
    • 需通过.value访问/修改数据(模板中自动解包,无需.value);
    • 支持所有数据类型,是Vue3中最灵活的响应式API。

2. reactive:引用类型的“代理响应式”

  • 定义reactive仅用于创建引用类型(Object、Array、Map、Set等)的响应式代理。
  • 原理:基于ES6的Proxy实现,直接代理整个对象,拦截对象的属性访问、赋值、删除等操作。
  • 关键特性
    • 无需.value,直接访问对象属性;
    • 不支持基本类型(传入基本类型会静默失效);
    • 不能直接替换整个对象(会丢失代理关系,导致响应式失效)。

3. ref与reactive核心区别

对比维度refreactive
支持数据类型所有类型(基本+引用)仅引用类型
访问方式.value(模板除外)直接访问属性
响应式原理Object.defineProperty + Proxy纯Proxy
替换对象支持(直接修改.value不支持(会丢失响应式)
解构特性解构后仍为响应式(ref类型)直接解构会丢失响应式(需toRefs)

二、10个实战案例解析

以下案例均基于Vue3的<script setup>语法(主流写法),涵盖基础用法、边界场景与最佳实践。

案例1:ref实现基本类型响应式(计数器)

需求:实现一个点击按钮递增的计数器。

<template>
  <div>计数:{{ count }}</div>
  <button @click="increment">+1</button>
</template>

<script setup>
import { ref } from 'vue'

// 1. 创建ref响应式数据(基本类型Number)
const count = ref(0)

// 2. 修改时需通过.value
const increment = () => {
  count.value += 1 // 必须加.value,否则无法触发响应式
}
</script>

解析ref(0)将基本类型0包装为响应式对象,模板中count自动解包,无需.value;JS逻辑中必须通过.value修改,否则无法触发视图更新。

案例2:reactive实现对象响应式(用户信息)

需求:展示并修改用户的姓名与年龄。

<template>
  <div>姓名:{{ user.name }}</div>
  <div>年龄:{{ user.age }}</div>
  <button @click="updateUser">更新信息</button>
</template>

<script setup>
import { reactive } from 'vue'

// 1. 创建reactive响应式对象(引用类型Object)
const user = reactive({
  name: '张三',
  age: 24
})

// 2. 直接修改属性,无需.value
const updateUser = () => {
  user.name = '李四' // 直接修改属性,触发响应式
  user.age += 1     // 支持属性的增量修改
}
</script>

解析reactive直接代理user对象,修改属性时无需额外语法,且深层属性(如user.name)自动具备响应式。

案例3:ref包裹引用类型(商品信息)

需求:用ref包裹对象,实现商品信息的修改。

<template>
  <div>商品名:{{ goods.name }}</div>
  <div>价格:{{ goods.price }}</div>
  <button @click="discount">打9折</button>
</template>

<script setup>
import { ref } from 'vue'

// 1. ref包裹引用类型(Object)
const goods = ref({
  name: 'Vue3实战教程',
  price: 99
})

// 2. 修改时需先通过.value访问对象,再修改属性
const discount = () => {
  goods.value.price *= 0.9 // 先.value获取对象,再改属性
}
</script>

解析:ref包裹引用类型时,内部会自动将对象转换为reactive代理,因此修改goods.value.price等同于修改reactive对象的属性,响应式依然生效。

案例4:reactive嵌套对象(订单信息)

需求:处理包含多层嵌套的订单数据,修改深层属性。

<template>
  <div>订单号:{{ order.id }}</div>
  <div>商品:{{ order.product.name }}</div>
  <div>状态:{{ order.status.text }}</div>
  <button @click="updateStatus">确认收货</button>
</template>

<script setup>
import { reactive } from 'vue'

// 1. reactive嵌套多层对象
const order = reactive({
  id: 'OD20240501',
  product: {
    name: '无线耳机',
    price: 399
  },
  status: {
    code: 1,
    text: '待收货'
  }
})

// 2. 直接修改深层属性,响应式依然生效
const updateStatus = () => {
  order.status.code = 2
  order.status.text = '已完成' // 深层属性修改,视图同步更新
}
</script>

解析reactive的Proxy代理会递归处理嵌套对象,无论嵌套多少层,修改属性都会触发响应式,无需额外处理。

案例5:ref数组(任务列表增删)

需求:实现任务列表的添加、删除功能。

<template>
  <ul>
    <li v-for="(task, index) in tasks" :key="index">
      {{ task }}
      <button @click="deleteTask(index)">删除</button>
    </li>
  </ul>
  <input v-model="newTask" type="text">
  <button @click="addTask">添加任务</button>
</template>

<script setup>
import { ref } from 'vue'

// 1. ref创建数组响应式数据
const tasks = ref(['学习ref', '学习reactive'])
const newTask = ref('') // 绑定输入框的响应式数据

// 2. 添加任务:修改数组引用(替换整个数组)
const addTask = () => {
  if (newTask.value.trim()) {
    // 方式1:用扩展运算符创建新数组(推荐,触发响应式)
    tasks.value = [...tasks.value, newTask.value]
    // 方式2:用数组方法(push/splice等,也支持)
    // tasks.value.push(newTask.value)
    newTask.value = '' // 清空输入框
  }
}

// 3. 删除任务:用splice修改数组
const deleteTask = (index) => {
  tasks.value.splice(index, 1) // 数组方法修改,触发响应式
}
</script>

注意:ref数组不推荐直接修改索引(如tasks.value[0] = '新任务'),可能导致响应式失效;推荐用push/splice或替换整个数组的方式。

案例6:reactive数组(商品列表筛选)

需求:实现商品列表的关键词筛选功能。

<template>
  <input v-model="filterText" placeholder="输入关键词筛选">
  <ul>
    <li v-for="item in filteredGoods" :key="item.id">
      {{ item.name }} - {{ item.price }}元
    </li>
  </ul>
</template>

<script setup>
import { reactive, computed } from 'vue'

// 1. reactive创建数组
const goodsList = reactive([
  { id: 1, name: 'Vue3教程', price: 99 },
  { id: 2, name: 'React教程', price: 89 },
  { id: 3, name: 'Vue3组件库', price: 129 }
])

// 2. ref绑定筛选输入框(基本类型用ref更合适)
const filterText = ref('')

// 3. 计算属性筛选数组
const filteredGoods = computed(() => {
  return goodsList.filter(item => 
    item.name.includes(filterText.value)
  )
})
</script>

解析:reactive数组支持直接用filter/map等数组方法,返回的新数组仍具备响应式;筛选关键词是基本类型,用ref更简洁。

案例7:ref与reactive转换(toRefs/toRef)

需求:将reactive对象解构为ref,保持响应式。

<template>
  <div>姓名:{{ name }}</div>
  <div>年龄:{{ age }}</div>
  <button @click="increaseAge">年龄+1</button>
</template>

<script setup>
import { reactive, toRefs, toRef } from 'vue'

// 1. 创建reactive对象
const user = reactive({
  name: '王五',
  age: 28
})

// 2. 用toRefs解构:所有属性转为ref(推荐批量解构)
const { name, age } = toRefs(user)

// 3. 用toRef单独转换某个属性(适用于单个属性)
// const age = toRef(user, 'age')

// 4. 修改时需通过.value(因解构后是ref类型)
const increaseAge = () => {
  age.value += 1 // 必须加.value,视图同步更新
}
</script>

关键:直接解构reactive对象(如const { name } = user)会丢失响应式;toRefs/toRef会创建与原对象关联的ref,修改后原对象也会同步更新。

案例8:与computed结合(计算总价)

需求:根据商品列表和数量,计算商品总价。

<template>
  <div>商品:{{ goods.name }}</div>
  <div>单价:{{ goods.price }}元</div>
  <div>数量:<button @click="decrease">-</button>{{ count }}<button @click="increase">+</button></div>
  <div>总价:{{ totalPrice }}元</div>
</template>

<script setup>
import { ref, reactive, computed } from 'vue'

// 1. reactive存储商品信息
const goods = reactive({
  name: '笔记本电脑',
  price: 5999
})

// 2. ref存储数量(基本类型)
const count = ref(1)

// 3. computed依赖ref和reactive数据
const totalPrice = computed(() => {
  return goods.price * count.value // 自动追踪依赖,缓存结果
})

// 4. 修改数量
const increase = () => count.value < 10 && (count.value += 1)
const decrease = () => count.value > 1 && (count.value -= 1)
</script>

解析computed会自动追踪goods.price(reactive属性)和count.value(ref值)的变化,当任一依赖更新时,totalPrice会重新计算。

案例9:生命周期中使用(挂载后初始化数据)

需求:组件挂载后,从接口(模拟)加载用户数据并渲染。

<template>
  <div v-if="loading">加载中...</div>
  <div v-else>
    <div>姓名:{{ user.name }}</div>
    <div>邮箱:{{ user.email }}</div>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'

// 1. ref存储加载状态(布尔值)
const loading = ref(true)

// 2. reactive存储用户数据(对象)
const user = reactive({
  name: '',
  email: ''
})

// 3. 生命周期钩子:挂载后加载数据
onMounted(async () => {
  // 模拟接口请求
  const mockApi = () => new Promise(resolve => {
    setTimeout(() => {
      resolve({ name: '赵六', email: 'zhao6@example.com' })
    }, 1000)
  })

  const data = await mockApi()
  // 赋值给reactive对象
  user.name = data.name
  user.email = data.email
  // 修改ref状态
  loading.value = false // 关闭加载态
})
</script>

解析:在onMounted中,可直接修改ref.valuereactive的属性,触发视图更新;loadingref(基本类型),userreactive(对象),符合两者的适用场景。

案例10:复杂嵌套场景(reactive含ref属性)

需求:处理复杂数据结构(reactive对象中包含ref属性)。

<template>
  <div>购物车商品数:{{ cart.count }}</div>
  <div>商品列表:</div>
  <ul>
    <li v-for="(item, index) in cart.items" :key="index">
      {{ item.name }} - {{ item.quantity }}件
      <button @click="addQuantity(index)">+1</button>
    </li>
  </ul>
</template>

<script setup>
import { reactive, ref } from 'vue'

// 1. reactive对象中包含ref属性
const cart = reactive({
  // count是ref类型(需.value修改)
  count: ref(0),
  // items是普通数组(reactive自动代理)
  items: [
    { name: '手机壳', quantity: 2 },
    { name: '充电器', quantity: 1 }
  ]
})

// 2. 修改ref属性(需.cart.count.value)
// 修改reactive数组属性(直接.item.quantity)
const addQuantity = (index) => {
  cart.items[index].quantity += 1 // 直接修改
  cart.count.value += 1 // 需.value(因count是ref)
}
</script>

注意:reactive对象中可嵌套ref,但修改时需多一层.value(如cart.count.value);实际开发中建议避免这种混合写法,优先保持数据结构统一。

三、最佳实践总结

  1. 数据类型优先原则

    • 基本类型(Number、String、Boolean):必用ref
    • 引用类型(Object、Array):优先用reactive,除非需要替换整个对象(此时用ref)。
  2. 避免响应式失效的坑

    • reactive不直接替换整个对象(如需替换,用ref包裹);
    • ref数组不直接修改索引(用push/splice或替换数组);
    • reactive解构用toRefs,避免直接解构丢失响应式。
  3. 复杂场景建议

    • 简单数据(如表单输入、计数器):用ref
    • 复杂对象(如用户信息、订单数据):用reactive
    • 组件间传参:ref更灵活(支持所有类型),reactive需配合toRefs传递。
  4. 性能考量

    • ref包裹引用类型时,内部会转为reactive,性能与reactive无差异;
    • 大量列表数据:优先用ref数组(修改时替换整个数组,性能更优)。

四、结语

refreactive是Vue3响应式系统的基石,理解二者的原理差异与适用场景,是写出高质量Vue3代码的关键。实际开发中无需纠结“非此即彼”,而是根据数据类型与业务需求灵活选择——让ref处理简单数据,让reactive管理复杂对象,再结合toRefs/computed等API,即可构建高效、可维护的响应式系统。

参考阅读: 🚀Vue3 响应式进阶:shallowRef与shallowReactive深度解析及10个实战案例