Vue3 响应式系统深度解析:ref 与 reactive

小飞兽 Vue.js 424 次阅读 2026-07-10

Vue3 响应式系统深度解析:ref 与 reactive

概述

Vue3 的响应式系统是整个框架的核心基石。理解 refreactive 的底层原理与适用场景,是写出高效、无 bug 的 Vue3 代码的前提。本文深入解析两种响应式创建方式的区别、实现机制、常见陷阱以及企业级项目中的最佳实践。

为什么响应式系统如此重要

Vue3 宣称"反直觉的变更检测"已成为历史。在 Vue2 中,由于 JavaScript 限制,某些操作(如数组索引赋值 arr[0] = newValue、添加新属性 obj.newKey = value)不会触发视图更新。Vue3 通过 ES6 Proxy 彻底解决了这个问题,但前提是你必须正确使用响应式 API。

基础语法

import { ref, reactive, toRefs, isRef, unref, shallowRef, triggerRef } from 'vue'

| API | 入参类型 | 返回值 | 特点 |
|-----|---------|--------|------|
| ref | 任意基本类型或对象 | Ref<T> 对象 | 基本类型需 .value,模板自动解包 |
| reactive | 对象/数组( Plain Object) | Proxy<T> 深度代理 | 直接操作属性,浅层解构丢失响应式 |
| toRefs | reactive 对象 | 每个属性转为 ref | 把 reactive 对象转 ref 集合,避免解构丢失 |
| isRef | 任意值 | boolean | 判断某值是否为 ref 对象 |
| unref | ref 或普通值 | 原始值 | 自动解包,ref 返回 .value,否则原样返回 |

完整代码示例

<script setup>
import { ref, reactive, toRefs, isRef, unref, shallowRef, triggerRef, computed, watch } from 'vue'

// ========== ref 的使用 ==========
// ref 包装基本类型
const count = ref(0)
const message = ref('Hello Vue3')
const isLoading = ref(false)

// 访问/修改 ref 的值需要 .value(模板中无需 .value)
function increment() {
  count.value++
}

// ref 也可以包装对象,Vue 会自动深层递归响应式处理
const user = ref({ name: '林小鸣', age: 28, scores: [90, 85, 92] })
function updateUser() {
  user.value.age = 29               // 直接修改属性,响应式
  user.value.scores.push(88)         // 数组操作也是响应式的
  user.value.newField = '新增字段'    // 新增属性同样响应式(Vue3 的 Proxy 优势)
}

// ========== reactive 的使用 ==========
// reactive 接收普通对象,返回深度代理
const state = reactive({
  title: 'Vue3 学习笔记',
  count: 0,
  metadata: {
    author: '林小鸣',
    tags: ['Vue3', 'JavaScript', '前端'],
    stats: { views: 1024, likes: 128 }
  }
})

// 直接访问/赋值属性
function addCount() {
  state.count++
}

// ========== ref vs reactive 的关键区别 ==========

// 1. reactive 对象解构会丢失响应式
const { title, count: dCount } = state  // dCount 和 title 是普通值,非响应式

// 正确做法:用 toRefs 转换
const { title: t, count: c } = toRefs(state)  // t 和 c 是 ref,保持响应式
// 使用时:t.value 访问,若在模板中可直接 {{ t }}

function updateFromRef() {
  // toRefs 后的 ref 与原始 reactive 数据完全同步
  t.value = '新标题'    // 修改 ref,原始 reactive state.title 同步更新
  c.value++             // 修改 ref,原始 state.count 同步增加
}

// 2. ref 的 isRef 和 unref 工具函数
console.log('isRef(count):', isRef(count))           // true
console.log('isRef(state.count):', isRef(state.count)) // false(普通数字)

// unref:自动去掉 ref 的 .value 外壳,获取原始值
function logValue(val) {
  const raw = unref(val)   // val 是 ref 则返回 .value,否则原样返回
  console.log('原始值:', raw)
}

// 3. 浅层响应式 shallowRef(性能优化场景)
// shallowRef 只追踪 .value 本身的替换,不会深度追踪对象内部变化
const shallow = shallowRef({ deep: '嵌套值' })
shallow.value.deep = '修改'    // 不会触发更新!只改变属性
shallow.value = { deep: '整体替换' }  // 这个才会触发更新

function triggerUpdate() {
  // 需要手动通知 shallowRef 更新
  triggerRef(shallow)
}

// 4. 监听 reactive/ref 变化
watch(count, (newVal) => {
  console.log('count 变化了:', newVal)
})

watch(state.metadata, (newMeta) => {
  // 监听嵌套对象,默认是深度监听
  console.log('metadata 变化:', newMeta)
}, { deep: true })
</script>

<template>
  <div>
    <h2>ref value: {{ count }} / {{ message }}</h2>
    <button @click="increment">+1</button>

    <hr />
    <h3>reactive 对象:{{ state.title }}</h3>
    <p>计数器: {{ state.count }}</p>
    <button @click="state.count++">reactive +1</button>
    <p>标签: <span v-for="tag in state.metadata.tags" :key="tag">{{ tag }} </span></p>

    <hr />
    <p>isRef(count) = {{ isRef(count) }}</p>
    <p>unref(count) = {{ unref(count) }}</p>
  </div>
</template>

运行效果说明

页面初始化显示计数器初始值。点击 "+1" 按钮,count.value 递增,页面数字同步更新。在 reactive 部分,点击按钮递增 state.count,视图同步响应。toRefs 转换后的变量在 JavaScript 中访问需要 .value,模板中直接使用变量名即可。shallowRef 在修改深层属性时不会触发更新,除非整体替换 .value

常见问题

Q1:为什么基本类型必须用 <code>ref</code> 而不是 <code>reactive</code>?

JavaScript 基本类型(number、string、boolean)不是对象,无法被 Proxy 代理。reactive 底层依赖 Proxy,只能接收对象类型。基本类型要实现响应式,只能通过 ref——它在内部创建一个包含 value 属性的响应式对象,通过 .value 的读写拦截实现响应式。

Q2:reactive 对象解构后为什么丢失响应式?如何安全解构?

解构 const { name } = reactive({...}) 时,name 是从 Proxy 上读取的普通值,没有经过响应式追踪。解决方案:① 使用 toRefs 将 reactive 对象每个属性转为 ref;② 直接在模板中使用 state.name 而不解构;③ 在 <script setup> 中保持 reactive 对象完整,避免解构。

Q3:shallowRef 和 ref 性能差异是什么?如何选择?

shallowRef 只对 .value 的整体替换做响应式追踪,不追踪对象内部的深层变化。当数据体量很大(如一个包含上万条数据的列表)且不需要深度监听时,用 shallowRef + 手动 triggerRef() 可显著减少响应式开销。常规开发中直接用 ref 即可,不要过早优化。

延伸阅读

  • <a href="#">Vue3 组合式 API 完全指南</a> —— 组合式 API 的完整介绍与项目结构最佳实践
  • <a href="#">Vue3 计算属性和监听器完全指南</a> —— computed、watch、watchEffect 的进阶用法
  • <a href="#">Vue3 依赖注入:provide/inject 详解</a> —— 跨层级传递响应式数据的标准方案