Vue 3 `<script setup>` 解构 props 后响应式丢了?一个「字段不更新」的排查实录

问题场景

项目是 Vue 3 + TypeScript + <script setup>,有个用户卡片组件:

vue 复制代码
<script setup lang="ts">
const props = defineProps<{
  user: { name: string; age: number }
  theme: 'light' | 'dark'
}>()

const { user, theme } = props
</script>

<template>
  <div :class="theme">
    <p>{{ user.name }} - {{ user.age }}岁</p>
  </div>
</template>

看起来很正常对吧?解构出来直接用,干净又优雅。

然而线上表现是 :父组件更新 user.name 后,页面上显示的 始终是旧值。只在切换路由或触发强制刷新后才看到新数据。

原因分析

Vue 3 的 defineProps 返回的是一个 reactive 响应式代理对象。以下两种写法的本质完全不同:

ts 复制代码
// ✅ 通过 props.xxx 访问 --- 保留响应式链路
const name = computed(() => props.user.name)

// ❌ const { user } = props --- 断开了响应式链条
const { user } = props
// user 是一个普通 JS 对象,和 props.user 不再有引用关系

解构赋值在编译时执行的是值拷贝 ------对基本类型(theme)拷贝值,对引用类型(user)拷贝引用地址。

听起来引用类型应该没问题?问题出在 Vue 3 的 Proxy 代理层props.user 返回的是一个代理过的对象,而解构出来的 user 拿到的是解构那一瞬间的快照 。Vue 的依赖追踪只追踪 props.user 这个 key 的访问,不会追踪解构后的局部变量。

层级越深越隐蔽

ts 复制代码
const { user } = props
// 后续:user.name = '新名字'
// Vue 的 set 拦截走的是 user 对象本身,不是 props.user
// 父组件通过 props.user.name = '新名字' 修改,
// 子组件的 user 变量仍然是旧的引用(如果是基本类型则不共享内存了)

核心一句话:解构把响应式代理的「代理」跳过了,你拿到的是原始数据的一次性拷贝。

解决方案

方案一:不用解构,直接 props.xxx

vue 复制代码
<script setup lang="ts">
const props = defineProps<{
  user: { name: string; age: number }
  theme: 'light' | 'dark'
}>()
</script>

<template>
  <div :class="props.theme">
    <p>{{ props.user.name }} - {{ props.user.age }}岁</p>
  </div>
</template>

模板里写 props. 有点啰嗦?Vue 3 模板中访问 props 不需要显式写 props.,直接用 {{ user.name }} 即可------模板编译时自动推导为 props.user.name

方案二:computed 转为可写响应式

当 JS 逻辑层需要操作 props 数据时:

ts 复制代码
const localUser = computed(() => props.user)
// ✅ localUser.value 始终跟随 props.user 变化

方案三:toRefs 保持单字段响应

ts 复制代码
const { user, theme } = toRefs(props)
// ✅ user 和 theme 都是 Ref,保持响应式
// 模板里用 user.name + user.age(不要 user.value,模板自动展开)

⚠️ 特别注意:defineProps 的 TS-only 语法

ts 复制代码
// 这是纯类型声明,没有运行时校验
defineProps<{ ... }>()

// 如果使用 withDefaults 给默认值,也不影响响应式特性
withDefaults(defineProps<{ count?: number }>(), {
  count: 0
})
// 解构后仍然会丢失响应式!

要点总结

写法 响应式 推荐
模板直接 {{ user.name }} ✅ 生产环境首选
const user = props.user 后模板使用 ❌ 不要解构
toRefs(props) ✅ 需要保留响应式时
computed(() => props.user) ✅ 有计算逻辑时
深度解构 const { name } = props.user ❌ 双重断开

黄金法则<script setup> 里对 defineProps 永远不要解构赋值 。模板里的变量会自动从 props 上读取,JS 逻辑层用 computedtoRefs 包装。

相关推荐
PBitW1 小时前
为什么vite中TS报错,可以继续运行?Webpack不行?💡
前端·typescript
GISer_Jing1 小时前
一套H5跑通三端:App+小程序内嵌H5跨端适配全栈解决方案
前端·前端框架·ai编程
码云之上1 小时前
项目团队从 5 人扩到 15 人,我写了个 CLI 让 IDE 共享 AI 规则
前端·人工智能·后端
飞天狗1 小时前
LCP / INP / CLS 三个维度的实战调优:把 Web Vitals 从不及格拉到优秀的完整路径
前端·性能优化
用户298698530141 小时前
前端 Excel 处理进阶:React 中合并与取消合并单元格的实现
前端·javascript·react.js
Highcharts2 小时前
@highcharts/react完整技术指南|新版Highcharts React使用、迁移、SSR、TS 优化全解
前端·javascript
老王以为2 小时前
Fiber 节点 —— 一个数据结构如何承载整个 React 运行时
前端·react native·react.js
2601_963771372 小时前
Optimizing Logistics and Transportation Sites on WordPress
前端·数据库·windows·php
c238562 小时前
HTTP 超文本传输协议全解:Web 世界的通信规则
前端·网络协议·http