父子组件通信是 Vue 应用中最基础、最频繁的数据传递方式。父组件通过 Props 向子组件传递数据,子组件通过 Emits 向父组件发送事件通知。Vue 还提供了 v-model 语法糖,让父子之间的“双向绑定”变得非常简洁。掌握这三种模式,就能覆盖绝大部分组件协作场景。
Props:父传子的单向数据通道
Props 是父组件传给子组件的自定义属性。子组件通过 defineProps 声明自己接受哪些属性,Vue 会在渲染时将父组件传递的值注入进来。
基础用法
<!-- 父组件 -->
<template>
<UserCard :name="userName" :age="30" />
</template>
<script setup>
import { ref } from 'vue'
import UserCard from './UserCard.vue'
const userName = ref('张三')
</script>
<!-- 子组件 UserCard.vue -->
<template>
<div>
<h3>{{ name }}</h3>
<p>年龄:{{ age }}</p>
</div>
</template>
<script setup>
const props = defineProps(['name', 'age'])
// 在模板和 script 中都可以直接通过 props.name 访问
console.log(props.name)
</script>
为了代码健壮性,强烈建议为 Props 指定类型和默认值,而不是只用一个字符串数组。
类型校验与默认值
<script setup>
const props = defineProps({
name: {
type: String,
required: true // 必填
},
age: {
type: Number,
default: 18 // 默认值
},
bio: {
type: String,
default: '' // 非必填时提供一个无害默认值
},
tags: {
type: Array,
default: () => [] // 对象/数组默认值必须用工厂函数
}
})
</script>
当父组件传入的 prop 类型不符时,Vue 会在控制台给出明确警告,这在团队协作和长期维护中非常有用。
单向数据流原则
Props 是只读的。子组件不应该直接修改 props 的值,因为这会破坏父组件数据的唯一数据源,导致状态混乱。Vue 会在开发模式下对修改 props 的行为抛出警告。
// ❌ 错误示范:直接修改 prop
props.name = '李四' // 警告:Avoid mutating a prop ...
// ✅ 正确做法:如果需要基于 prop 衍生本地状态,用 ref 或 computed
import { ref, computed } from 'vue'
const internalName = ref(props.name) // 本地拷贝
const displayName = computed(() => props.name.toUpperCase()) // 只读计算
如果你确实需要“修改”一个 prop(比如父组件将某个状态的控制权交给子组件),那就应该使用接下来的 事件通知 或 v-model 模式。
Emits:子传父的事件通知
子组件不能修改 props,但可以触发自定义事件,通知父组件“该做什么”。父组件监听到事件后,在自己的上下文里处理逻辑(如修改数据),数据再通过 props 回流到子组件,形成闭环。
基础用法
<!-- 子组件 ConfirmButton.vue -->
<template>
<button @click="handleClick">确认</button>
</template>
<script setup>
const emit = defineEmits(['confirm']) // 声明可触发的事件名
function handleClick() {
emit('confirm', '订单已提交') // 触发事件,可传递参数
}
</script>
<!-- 父组件 -->
<template>
<ConfirmButton @confirm="showResult" />
<p>{{ message }}</p>
</template>
<script setup>
import { ref } from 'vue'
import ConfirmButton from './ConfirmButton.vue'
const message = ref('')
function showResult(text) {
message.value = text // 父组件处理逻辑
}
</script>
参数传递规范
事件参数可以是任意 JavaScript 值。如果参数较多,建议封装成一个对象,提高可读性:
emit('submit', { id: 1, title: '新文章', content: '...' })
// 父组件: @submit="handleSubmit" → handleSubmit(payload) { ... }
类型安全的 Emits 声明(TypeScript)
<script setup lang="ts">
const emit = defineEmits<{
(e: 'confirm', text: string): void
(e: 'submit', payload: { id: number; title: string }): void
}>()
</script>
这样父组件的事件处理函数就能获得精确的类型推断。
v-model:父子之间的双向绑定
v-model 本质上是 Props + Emits 的语法糖,让父子组件之间像表单元素一样实现“双向绑定”。它最常见于封装表单控件、弹窗显示控制等场景。
基础用法(Vue 3.4 之前)
在子组件中,你需要接收一个 modelValue prop,并在值变化时触发 update:modelValue 事件。
<!-- 子组件 MyInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<!-- 父组件 -->
<template>
<MyInput v-model="text" />
<p>你输入了:{{ text }}</p>
</template>
<script setup>
import { ref } from 'vue'
const text = ref('')
</script>
v-model 解构(Vue 3.4+ 的简化写法)
Vue 3.4 引入了 defineModel 宏,让自定义 v-model 的写法大幅精简:
<!-- 子组件 MyInput.vue -->
<template>
<input v-model="value" />
</template>
<script setup>
const value = defineModel() // 返回一个 ref,直接用于双向绑定
</script>
defineModel() 内部自动处理了 modelValue 的 prop 和 update:modelValue 的事件,使用时就像在管理一个本地 ref 一样简单。如果需要指定默认值:defineModel({ default: '' })。
多个 v-model 绑定
一个组件可以同时绑定多个 v-model,每个都对应一个独立的 prop 和事件:
<!-- 父组件 -->
<UserInfo v-model:name="userName" v-model:age="userAge" />
<!-- 子组件 UserInfo.vue -->
<template>
<input :value="name" @input="$emit('update:name', $event.target.value)" />
<input :value="age" @input="$emit('update:age', $event.target.value)" />
</template>
<script setup>
defineProps(['name', 'age'])
defineEmits(['update:name', 'update:age'])
</script>
如果使用 Vue 3.4+,可以简写为:
<script setup>
const name = defineModel('name')
const age = defineModel('age')
</script>
v-model 修饰符的内置与自定义
Vue 内置了 .lazy、.number、.trim 修饰符,也支持开发者自定义修饰符。
- 内置修饰符示例:
<MyInput v-model.trim="text" />会自动去除首尾空格。 - 自定义修饰符:子组件通过
defineModel或 props 的modelModifiers属性访问修饰符,并手动处理逻辑。
例如,实现一个自动将英文转为大写的 capitalize 修饰符:
<!-- 子组件 MyInput.vue -->
<template>
<input
v-model="internalValue"
@input="handleInput"
/>
</template>
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue'])
const internalValue = ref(props.modelValue)
function handleInput(e) {
let value = e.target.value
if (props.modelModifiers.capitalize) {
value = value.toUpperCase()
}
emit('update:modelValue', value)
}
</script>
父组件使用:<MyInput v-model.capitalize="text" />。
父子通信的核心原则
- Props 向下,Events 向上:永远不要通过 props 直接修改父组件数据,也不要通过
$refs在子组件里强行修改父组件状态。严格的单向数据流让应用的数据变化路径可预测、可调试。 - 优先用 v-model 双向绑定:当父子组件共享一个状态且子组件需要“修改”该状态时(如表单控件、开关组件),v-model 是最简洁、最符合直觉的方案。
- 复杂的通信改用依赖注入或状态管理:如果组件层级超过两级,或者需要跨多级组件传递数据,不要硬套 props 和 emits(传递链条太长)。下一节(9.2)会介绍
provide / inject等方案。 - 类型和校验是免费的保险:无论项目大小,给每个 prop 加上类型和默认值,花五分钟写,能省好几个小时的排错时间。
掌握了这三种模式,你就已经可以构建出结构清晰、职责分明的组件树了。接下来,我们会看到如何打破层级限制,进行跨层级通信。