Vue3 响应式数据常用方案及实践坑点

最近在做 Vue 项目相关的需求,复习一下 Vue 的响应式机制及其常用办法

从 Vue3 视角来看,它的响应式数据核心就是refreactive,都依赖于 ES6 的Proxy API,以此来代理监听整个对象,从而能关注到复杂数据类型内部属性的增删改的变化。值得一提的是,这里代理对象包含了多嵌套式对象的情况,也就是可以实现深度监听。

相较于 Vue2 的defineProperty()仅对于属性层面的监听,无疑在构建复杂数据类型的响应式时,性能提升是巨大的。

下面让我们聊聊无处不在的refreactive:

ref

ref通常用来包装基本数据类型,由于Proxy是对于复杂数据类型的 API,所以它的实质是在Proxy包装的基础上又在外封装了一层,所以我们需要用.value来读写数据,但在<template>模板中访问响应式数据无需.value因为此时已经做了解包的处理。

reactive

对于reactive相对的便是用来包装复杂数据类型,诸如ObjectArray这样的数据,他可以直接监听整个对象的属性操作(增删改)。但要注意的是,切勿直接操作这个对象,也就是说不要改变这个reactive数据的引用,这会使他丢失响应式。

二者怎么抉择呢,尤大大提倡使用ref,事实也正是如此,绝大多数场景,简单和复杂数据类型均使用ref构建响应式,虽然理论上全部加一层包装会有性能损耗,但对于团队代码可读性和可维护性,这点损耗微乎其微。下面举个例子:

js 复制代码
// 情景:初始化一个 list,后续调接口拿到数据 res.data,需要赋值(先不考虑使用 TS)
// 使用 reactive
const list = reactive({})
Object.keys(res.data).forEach(key => {
    list.key = res.data[key]
})
// 使用 ref
const list = ref({})
list.value = res.data
// 或更严谨
list.value = {...list.value, ...res.data}

高下立判,无论从可读性还是维护性上讲ref也是完胜的。当然对于一些构造表单模板即对象属性增删不频繁的场景reactive不免为更优雅的选择...

以上是在学习阶段对于两个兄弟的基本认识。

响应式数据在组件间通信

说起这点,最常用的便是父子组件间props+emit的通信

父→子:通过props传给子组件,子组件可直接使用,值得一提的是这里的props虽然是响应式的但我们不能直接通过props.a来修改,这虽然可行但违背了 Vue 单向数据流的原则会报错,试想如果一个响应式数据想在哪里修改就在哪里修改,姑且不说可能导致的异常,就代码规范性而言就不过关

子→父:所以我们通过emit的方法来修改,通过$emit触发父组件传给子组件的事件类型,父组件监听并响应触发事件

这里以 Vue3 组合式 API 的写法为例:

js 复制代码
// Parent.vue -->
<template>
  <Child :count="count" @update-count="handleUpdate" />
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const count = ref(0)
const handleUpdate = (newVal) => {
  count.value = newVal
}
</script>

// Child.vue -->
<template>
  <button @click="update">Count: {{ count }}</button>
</template>

<script setup>
const props = defineProps(['count'])
const emit = defineEmits(['update-count'])

const update = () => {
  emit('update-count', props.count + 1)
}
</script>

这里仅仅讲述最常用的通信,还有provide+injectpinia/vuex这里便不再赘述

常见坑点

我们在接收到porps的数据以后,如果父组件传的是一个ref,或者是reactive,或者是非响应式?我们子组件接受到该怎么使用,需要加.value?可以直接使用?还是需要传给中间值?怎么传?

这些问题可能在学习阶段无需思考,已经知道了怎么用就顺着来写,但我们需要考虑的是如果好久不用了,我们能否通过自己的技术深度来知道怎么使用是正确的,怎么使用不会丢失响应式?不会导致异常?

1、如果传值是ref/reactive/非响应,子组件如何用?

结论 :无论父组件传的是 refreactive 还是普通对象,子组件通过 props 接收到的都是一个「普通响应式对象」,也就是Proxy,你永远不需要、也不应该在子组件中对props使用.value

原因:Vue 对 props 的统一处理机制

当你在父组件这样传递数据:

js 复制代码
// 父组件
const a = ref({ name: 'Alice' })        // ref
const b = reactive({ name: 'Bob' })     // reactive
const c = { name: 'Charlie' }           // 普通对象

<Child :data-a="a" :data-b="b" :data-c="c" />

Vue 在传递给子组件前,会自动将所有值标准化为响应式对象 (如果还不是的话),并注入到 props 中。

子组件接收到的 props 是一个由 Vue 内部创建的 响应式 Proxy 对象,结构如下:

js 复制代码
// 子组件中的 props(概念上)
props = reactive({
  dataA: { name: 'Alice' },   // ← 已解包 ref,并转为响应式
  dataB: { name: 'Bob' },     // ← 原 reactive 对象(或其代理)
  dataC: { name: 'Charlie' }  // ← 普通对象被自动 reactive 包装
})

所以:props 中的每个属性都已经是"解包后"的响应式对象,无需 .value

2、该如何使用?

结论 :始终通过 props.xxx 访问数据(不解构、不赋值给顶层变量),并在需要修改但不影响源数据时创建本地副本。

原因

先看个错误的示例:

❌ 错误做法:解构或顶层赋值

js 复制代码
setup(props) {
  const { name } = props.user;     // ❌ name 是普通字符串,失去响应式
  const age = props.user.age;      // ❌ age 是快照引用,不会随父更新

  // 后续使用 name/age 都是非响应式的!
}

有的兄弟可能要说了,我们有时候就只是需要其中的一个属性数据,也不用响应式,这样直接拿到不就好了?

但是请注意,如果父组件传的数据是异步获取的,当你直接结构或取值时可能拿到的是执行完异步操作前的数据,也就是说可能永远拿到的都是初始化时的空数据,因为就算异步操作完成,也会因丢失响应式而不会更新数据,造成问题!

🔔 ESLint 规则 vue/no-setup-props-destructure 就是为了防止这类错误。

所以始终通过 props.xxx 访问数据(不解构、不赋值给顶层变量)

而当我们本地需要创建副本来维护这个数据,但不影响父组件时:

js 复制代码
import { ref, watch } from 'vue'
import _ from 'lodash' // 或自定义 deepClone

setup(props) {
  // 创建深度独立副本(保持本地响应式)
  const localUser = ref(_.cloneDeep(props.user));

  // 可选:监听 prop 变化以重置本地状态(如父组件刷新数据)
  watch(() => props.user, (newUser) => {
    localUser.value = _.cloneClone(newUser);
  });

  const updateName = (name) => {
    localUser.value.name = name; // ✅ 修改本地副本,不影响父组件
  };

  return { localUser, updateName };
}

终极建议:

  • 模板中 :直接写 {{ props.xxx.yyy }}

  • setup 中

    • 只读 → 用 props.xxx() => props.xxx(在 watch/computed 中)
    • 需修改 → 创建 ref(deepClone(props.xxx)) 作为本地状态
  • 绝不在 setup 顶层解构 props 或赋值给普通变量

  • 修改数据 → 通过 emit 通知父组件,或操作本地副本

实力不济,新人小白,持续更新...

相关推荐
随风一样自由10 小时前
【前端独角兽】刚进入前端领域的前端开发用jQuery还是Vue3?
前端·javascript·vue3·jquery
IMPYLH11 小时前
HTML 的 <data> 元素
前端·html
YHHLAI11 小时前
[特殊字符] 面试中的 Promise —— 从入门到精通
前端·javascript·面试
leoZ23113 小时前
Claude 驱动的全栈开发:Spring Boot + Vue 的 BS 架构实践
vue.js·spring boot·架构
kyriewen13 小时前
我扒了 GPT-5.6 的全部编程跑分——有一项 OpenAI 没敢放出来
前端·gpt·ai编程
前端H14 小时前
微前端 v3 架构升级:从加载隔离到状态协同
前端·架构·状态模式
陆枫Larry15 小时前
小程序包体积优化:用 SVG 图片替换 iconfont,并保留 CSS 控色能力
前端
环境栈笔记16 小时前
多账号浏览器选型检查清单:Profile、权限、Session 和任务日志怎么评估
前端·人工智能·后端·自动化
前端炒粉16 小时前
手写promise
java·前端·javascript
LaughingZhu16 小时前
智能体经典范式构建:ReAct、Plan-and-Solve 与 Reflection
前端·react.js·前端框架