问题场景
项目是 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 逻辑层用 computed 或 toRefs 包装。