02|Props 输入契约
第二章:组件模型与通信纵览 / 02。组件可以复用,前提是调用方能给每个实例不同输入,同时仍保留输入的所有权。
前置与运行方式
继续使用 Vue 3.5 SFC 环境。下面的 ProductCard.vue 与 App.vue 放在同一目录即可运行;本页只让父级改变数据,子级尚不向外发事件。
最小可运行合同
<script setup lang="ts">
interface Props {
name: string
price: number
featured?: boolean
}
const props = withDefaults(defineProps<Props>(), {
featured: false
})
</script>
<template>
<article>
<strong>{{ props.name }}</strong>
<span>¥{{ props.price.toFixed(2) }}</span>
<small v-if="props.featured">推荐</small>
</article>
</template><script setup lang="ts">
import { ref } from 'vue'
import ProductCard from './ProductCard.vue'
const price = ref(19.9)
</script>
<template>
<ProductCard name="Vue 手册" :price="price" featured />
<button type="button" @click="price += 10">父级涨价</button>
</template>注意静态和动态传值的差别:name="Vue 手册" 总是字符串,:price="price" 会先求表达式的值,裸写 featured 等价于传入 true。
输入进入组件的中间过程
| 阶段 | name | price | featured |
|---|---|---|---|
| 父模板求值 | 字符串字面量 | 读取父级 price ref | 裸布尔属性 |
| 组件接收 | string | number | true |
| 子模板读取 | props.name | props.price | props.featured |
| 父级点击后 | 不变 | 新值向下传入 | 不变 |
defineProps 是 <script setup> 的编译宏,不需要导入。类型声明会帮助编译器生成相应的运行时 props 选项,但 TypeScript 不能替代所有业务校验,例如“价格必须大于等于零”。
默认值的两种写法
Vue 3.5 可以直接对 defineProps 做响应式解构并给默认值:
const { name, featured = false } = defineProps<{
name: string
featured?: boolean
}>()编译器会追踪这些解构变量对应的 prop 更新。若代码需要兼容更早的 Vue 3 版本,或团队想保留显式的 props.x 读取,使用正文里的 withDefaults 更容易统一。用 withDefaults 给数组、对象设置默认值时要返回新实例:
const props = withDefaults(defineProps<{ tags?: string[] }>(), {
tags: () => []
})否则多个组件实例可能拿到同一个可变默认对象。
只读为什么是“浅”的
直接赋值会破坏单向数据流:
props.price++ // TypeScript 报错;开发环境也会警告每次父组件更新时,新的 prop 值都会再次向下流。即使某次强行改动看似成功,也会被后续更新覆盖。
但对象和数组 prop 的内部成员并不会被深度冻结:
interface User { name: string }
const props = defineProps<{ user: User }>()
props.user.name = '被子组件改了' // 语法可行,却改到了父级持有的同一对象这正是“浅只读”的边界。除非父子紧密协作且合同明确,否则子组件应把变化作为事件交回父级;下一篇会补上这条回程通道。
运行时校验用于开发反馈
需要表达“非负数”时,可以改用运行时声明:
const props = defineProps({
price: {
type: Number,
required: true,
validator: (value: number) => value >= 0
}
})校验失败会在开发环境给出警告,不会自动阻止组件渲染,也不是服务端数据验证。组件仍要决定错误值显示什么;安全或数据完整性检查仍应放在可信边界。
布尔 Prop 的容易误判处
布尔 prop 会按声明类型进行转换。若联合类型同时包含 Boolean 与 String,声明顺序会影响空属性的结果。不要靠隐式转换表达复杂业务状态;loading、disabled 适合布尔值,“自动/开启/关闭”更适合明确联合类型。
练习:只读库存徽标
实现 StockBadge.vue 和父级 App.vue,合同如下:
- props 为
label: string、count: number、showZero?: boolean,其中showZero默认true。 count > 0显示“label:剩余 n 件”;count === 0且showZero为真时显示“已售罄”,为假时不渲染徽标。count < 0显示“库存数据错误”,不能把 prop 改成0来掩盖错误。- 父级提供“补货”和“清空”按钮,所有库存修改都发生在父级。
- 分别验证
2、0、-1与未传showZero四种输入。
参考答案:查看第 2 章第 02 题参考答案。
通关自检:你能解释为什么“不能给 prop 重新赋值”不等于“对象 prop 已经深冻结”,并指出业务校验与 TypeScript 类型各自负责什么。