Vue 3 组件通信实战系列(一)父子组件通信的标准姿势:Props 与 Emit(含实战与进阶技巧)
在 Vue 3 中,组件通信是项目开发的核心能力之一,而父子组件通信是最基础也是最常用的通信方式。
相比 Vue 2,Vue 3 带来了 Composition API、TypeScript 类型增强、<script setup>
等现代特性,使得父子通信的实践方式发生了显著变化,既更简洁,又更类型安全。
本篇文章将手把手带你掌握:
props
:父传子数据的标准用法、类型定义与默认值技巧emit
:子传父事件的声明方法、参数校验与类型提示- 封装通信逻辑、提高复用性的方法
- 常见踩坑场景与规范总结
一、父传子通信:Props 的标准与进阶用法
1.1 什么是 Props?
Props 是组件间通信中用于 从父组件向子组件传递数据 的方式。在 Vue 中,它们是单向绑定的,只能由父组件控制,子组件只能"接收"和"消费"。
1.2 基础使用案例
👨👩👧 父组件:传入数据
xml
<!-- Parent.vue -->
<template>
<Child :title="pageTitle" :count="counter" />
</template>
<script setup>
import Child from './Child.vue'
const pageTitle = '欢迎来到 Vue 组件通信世界'
const counter = 3
</script>
👶 子组件:接收 Props
xml
<!-- Child.vue -->
<script setup>
defineProps(['title', 'count'])
</script>
<template>
<h2>{{ title }}</h2>
<p>当前数量:{{ count }}</p>
</template>
🔎 **注意:**这是一种快速声明方式,适合原型开发。但推荐在正式开发中使用更安全的方式:泛型 + 类型定义。
1.3 推荐方式:使用 TypeScript 泛型定义 Props 类型
xml
<script setup lang="ts">
defineProps<{
title: string
count: number
}>()
</script>
优点:
- 明确参数类型
- 支持 IDE 智能提示
- 提高可维护性,降低低级错误发生概率
1.4 设置默认值:withDefaults 的正确用法
有时候,父组件可能不传某个 prop。为了保证子组件的健壮性,我们可以设置默认值。
xml
<script setup lang="ts">
const props = withDefaults(defineProps<{
title?: string
count?: number
}>(), {
title: '默认标题',
count: 0
})
</script>
💡提示: 默认值用于可选属性(?
),确保子组件不会因数据缺失而崩溃。
1.5 高阶示例:接收复杂对象、数组
typescript
<script setup lang="ts">
interface UserInfo {
name: string
age: number
}
const props = withDefaults(defineProps<{
user: UserInfo
}>(), {
user: () => ({
name: '匿名用户',
age: 18
})
})
</script>
<template>
<p>{{ user.name }},{{ user.age }} 岁</p>
</template>
⚠️ 对象和数组默认值必须是函数返回,否则会出现多个组件实例共享同一引用的风险。
1.6 不能修改 Props:单向数据流约束
Vue 强调单向数据流。子组件不能直接修改 props:
arduino
props.count++ // ❌ 会报错:Cannot assign to read only property
正确做法是:通过 emit
通知父组件更新(见下一节)。
二、子传父通信:Emit 的标准与类型安全用法
2.1 什么是 Emit?
子组件通过 emit
发出事件,父组件监听该事件并处理。这是 子组件向父组件传递消息或数据的标准方式。
2.2 基础用法示例
👶 子组件
xml
<script setup lang="ts">
const emit = defineEmits(['update-count'])
function update() {
const newCount = Math.floor(Math.random() * 100)
emit('update-count', newCount)
}
</script>
<template>
<button @click="update">点击更新 Count</button>
</template>
👨👩👧 父组件
xml
<template>
<Child @update-count="handleCount" />
</template>
<script setup>
function handleCount(newCount: number) {
console.log('子组件更新了 count:', newCount)
}
</script>
2.3 推荐方式:为 Emit 定义类型签名
typescript
const emit = defineEmits<{
(e: 'update-count', payload: number): void
}>()
优势:
- 事件名提示
- 参数类型校验
- IDE 支持更完整
2.4 常见通信模式:子组件通知父组件修改数据
这是典型的**"子传父 -> 父传子"**模式,配合 v-model
使用更优雅(后续专题讲 v-model 双向绑定):
scss
// 子组件内部触发修改
emit('update:modelValue', newValue)
配合父组件:
ini
<Child v-model="value" />
三、封装通信逻辑:useXXX 模式的实战应用
在中大型项目中,通信逻辑可以通过组合式函数封装,提高代码复用与清晰度。
typescript
// useDialog.ts
export function useDialog(props: { visible: boolean }, emit: (e: 'update:visible', v: boolean) => void) {
const close = () => emit('update:visible', false)
return { isVisible: props.visible, close }
}
在组件中:
arduino
<script setup>
const props = defineProps<{ visible: boolean }>()
const emit = defineEmits<{
(e: 'update:visible', v: boolean): void
}>()
const { isVisible, close } = useDialog(props, emit)
</script>
四、常见问题与错误用法
问题 | 原因 | 解决方案 |
---|---|---|
props 值为只读 | Vue 设计为单向数据流 | 使用 emit 通知父组件修改 |
defineEmits 不定义事件 |
会失去类型提示 | 显式声明每个事件和参数类型 |
默认值为对象但未使用函数 | 多组件共享同一引用 | 使用函数返回对象作为默认值 |
不使用泛型定义 props | 缺乏类型提示 | 使用泛型进行明确声明 |
五、最佳实践与总结
- 所有
props
和emit
都应进行类型声明; - 推荐使用
<script setup>
结合 Composition API; - 使用
withDefaults
设置合理默认值; - props 为"读",emit 为"写",保持组件单向数据流;
- 复用通信逻辑推荐抽离为组合式函数(
useXXX
)模块;
下一篇预告
在下一篇文章中,我们将深入探索「兄弟组件通信」的多种方式,包括:
- mitt 全局事件总线的使用
provide/inject
跨层级传值- 中间组件代理通信
敬请期待:Vue 3 组件通信系列(二):兄弟组件如何优雅通信?
如果你觉得这篇文章对你有帮助:
👉 欢迎点赞、收藏、关注专栏
📌 持续更新「Vue 3 组件通信实战系列」
🧑💻 项目实战代码模板、专属源码包可私信获取