Skip to content

02|Props 输入契约 ​

第二章:组件模型与通信纵览 / 02。组件可以复用,前提是调用方能给每个实例不同输入,同时仍保留输入的所有权。

前置与运行方式 ​

继续使用 Vue 3.5 SFC 环境。下面的 ProductCard.vue 与 App.vue 放在同一目录即可运行;本页只让父级改变数据,子级尚不向外发事件。

最小可运行合同 ​

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>
vue
<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。

输入进入组件的中间过程 ​

阶段namepricefeatured
父模板求值字符串字面量读取父级 price ref裸布尔属性
组件接收stringnumbertrue
子模板读取props.nameprops.priceprops.featured
父级点击后不变新值向下传入不变

defineProps 是 <script setup> 的编译宏,不需要导入。类型声明会帮助编译器生成相应的运行时 props 选项,但 TypeScript 不能替代所有业务校验,例如“价格必须大于等于零”。

默认值的两种写法 ​

Vue 3.5 可以直接对 defineProps 做响应式解构并给默认值:

ts
const { name, featured = false } = defineProps<{
  name: string
  featured?: boolean
}>()

编译器会追踪这些解构变量对应的 prop 更新。若代码需要兼容更早的 Vue 3 版本,或团队想保留显式的 props.x 读取,使用正文里的 withDefaults 更容易统一。用 withDefaults 给数组、对象设置默认值时要返回新实例:

ts
const props = withDefaults(defineProps<{ tags?: string[] }>(), {
  tags: () => []
})

否则多个组件实例可能拿到同一个可变默认对象。

只读为什么是“浅”的 ​

直接赋值会破坏单向数据流:

ts
props.price++ // TypeScript 报错;开发环境也会警告

每次父组件更新时,新的 prop 值都会再次向下流。即使某次强行改动看似成功,也会被后续更新覆盖。

但对象和数组 prop 的内部成员并不会被深度冻结:

ts
interface User { name: string }
const props = defineProps<{ user: User }>()
props.user.name = '被子组件改了' // 语法可行,却改到了父级持有的同一对象

这正是“浅只读”的边界。除非父子紧密协作且合同明确,否则子组件应把变化作为事件交回父级;下一篇会补上这条回程通道。

运行时校验用于开发反馈 ​

需要表达“非负数”时,可以改用运行时声明:

ts
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 类型各自负责什么。

用心记录每一步成长