在 Vue 的组件树中,父组件向子组件传递数据的基本方式就是 Props。你可以把它理解成组件的“公开属性接口”——父组件通过给子组件标签添加属性的方式传值,子组件显式声明自己接受哪些属性,然后就能像使用本地数据一样使用这些传入的值。
基本用法:从父组件传,在子组件收
父组件传递数据:
<!-- 静态值 -->
<ChildComponent title="用户列表" />
<!-- 动态值 -->
<ChildComponent :user="currentUser" :count="5" />
子组件接收数据(组合式 API):
<script setup>
const props = defineProps({
title: String,
user: Object,
count: Number
})
// 直接在模板中使用 {{ title }},在逻辑中使用 props.title
console.log(props.title)
</script>
子组件接收数据(选项式 API):
export default {
props: ['title', 'user', 'count'],
// 或者带类型声明的对象形式
props: {
title: String,
user: Object,
count: Number
}
}
虽然简洁的数组形式也能用,但强烈建议始终使用对象形式,因为它开启了类型校验、默认值等能力,也方便其他开发者理解你的组件需要什么参数。
类型校验:让传入的数据符合预期
Vue 允许为每个 prop 指定一个或多个类型,当传入的值类型不符时,会在控制台发出警告(开发模式下),帮助你快速定位数据流转中的错误。
支持的类型:
- 原生构造函数:
String、Number、Boolean、Object、Array、Function、Symbol、Date - 也可以传入
null表示允许任意类型
defineProps({
name: String,
age: Number,
isAdmin: {
type: Boolean, // 当只写 Boolean 类型时,父组件省略该属性会默认为 false
},
metadata: Object
})
多类型校验:一个 prop 允许几种类型时,用数组:
status: {
type: [String, Number], // 接受字符串 'active' 或数字 1
required: true
}
自定义校验函数:validator 属性能实现复杂的校验逻辑,返回 true 表示通过,false 则报警告。
age: {
type: Number,
validator: (value) => {
// 年龄必须为整数且大于 0
return Number.isInteger(value) && value > 0
}
}
实用建议:类型校验不会阻止组件的渲染,它只是控制台警告。但在多人协作或代码重构时,这些警告能大量减少“类型对不上导致页面逻辑异常”的隐蔽 bug。
默认值:未传值时保证组件正常运行
使用 default 属性可以为 prop 设置一个后备值,当父组件没有传递该 prop 时,子组件会使用这个默认值。
defineProps({
pageSize: {
type: Number,
default: 10 // 默认每页10条
},
showHeader: {
type: Boolean,
default: true // 默认显示表头
},
config: {
type: Object,
// 对象或数组的默认值必须从一个工厂函数返回
default: () => ({
theme: 'light',
size: 'medium'
})
}
})
关键细节:
- 对于
Object或Array类型,default必须是函数,否则多个组件实例会共享同一个引用,引发数据污染。 - 默认值可以是任何有效的值,甚至可以是
null。
必填校验:确保关键数据不缺位
设置 required: true 后,如果父组件未传入该 prop,控制台会报警告。这适用于组件运行必不可少的参数,比如展示用户卡片必须要有 userId。它和 default 是互斥的——必填就不需要默认值。
defineProps({
userId: {
type: String,
required: true
},
userName: {
type: String,
required: false // 非必填,可以省略,等价于不写 required
}
})
单向数据流:一个铁律,也是避坑指南
这是 Props 机制中最重要的概念:所有的 prop 都在父组件与子组件之间形成一个单向向下的绑定。父组件的更新会向下流动到子组件,但反过来不行。这是为了防止子组件意外地改变父组件的状态,导致数据流向难以追踪。
为什么必须单向?
如果允许子组件直接修改 prop,当父组件的同一份数据同时传给了多个子组件时,某一个子组件的修改会让所有兄弟组件的数据都受到意外影响,调用链将变得混乱不堪。单向数据流保证了数据源有唯一的主人(父组件),任何让数据发生变化的意图,都必须由父组件自行处理。
违反时会怎样?
Vue 会在控制台警告:“Avoid mutating a prop directly”,提醒你这种操作是不被允许的。虽然技术上在某些情况下修改对象属性可能不会报错(因为引用相同),但这是极度危险的写法,会带来难以排查的状态错乱。
子组件想让数据变,怎么办?
- 向上传递事件:子组件通过
$emit通知父组件“我想要改这个值”,父组件在对应的事件处理函数中修改数据,然后新的值通过 props 再次流回子组件。
<!-- 子组件 -->
<script setup>
defineProps({ modelValue: Boolean })
const emit = defineEmits(['update:modelValue'])
function handleClose() {
emit('update:modelValue', false) // 告诉父组件我要关闭了
}
</script>
- 使用
v-model:这是一种简化的双向绑定语法,本质也是 “prop + update 事件” 的封装。在表单组件中尤其常用,但在语义上它仍然遵循单向数据流——数据的修改权仍然在父组件手中,只是模板写法更简洁了。
<!-- 父组件 -->
<MyInput v-model="username" />
<!-- 等价于 -->
<MyInput :modelValue="username" @update:modelValue="username = $event" />
实用规则总结:
- 子组件永远不要直接修改 prop。
- 如果需要改变,通知父组件(emit)或者基于 prop 创建一个本地的派生状态(如用
ref初始化后自己维护,但此时本地的状态将独立于父传值变化,要清楚语义)。 - 对于引用类型 prop,如果只是读取其属性(如
user.name),不修改对象本身,是完全合法的。
遵循这些约束,props 就能构建出一个清晰、可预测、易于调试的组件通信体系。