人人都会AI编程

9.1 父子组件通信

更新时间:2026-07-11

父子组件通信是 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" />


父子通信的核心原则

  1. Props 向下,Events 向上:永远不要通过 props 直接修改父组件数据,也不要通过 $refs 在子组件里强行修改父组件状态。严格的单向数据流让应用的数据变化路径可预测、可调试。
  2. 优先用 v-model 双向绑定:当父子组件共享一个状态且子组件需要“修改”该状态时(如表单控件、开关组件),v-model 是最简洁、最符合直觉的方案。
  3. 复杂的通信改用依赖注入或状态管理:如果组件层级超过两级,或者需要跨多级组件传递数据,不要硬套 props 和 emits(传递链条太长)。下一节(9.2)会介绍 provide / inject 等方案。
  4. 类型和校验是免费的保险:无论项目大小,给每个 prop 加上类型和默认值,花五分钟写,能省好几个小时的排错时间。

掌握了这三种模式,你就已经可以构建出结构清晰、职责分明的组件树了。接下来,我们会看到如何打破层级限制,进行跨层级通信。