第7课 组件通信:Props 与 Emit
- 能将页面拆分为独立的组件并组合使用
- 掌握 Props 实现父组件向子组件传递数据(类型校验、默认值)
- 掌握 Emit 实现子组件向父组件通信(自定义事件传参)
- 理解单向数据流与"数据在谁手里,逻辑就在谁那里"的设计原则
1. 组件的基础使用
组件就是一个独立的 .vue 文件,自带 template、script、style 三件套。使用组件需要三步:
| 步骤 | 操作 | 写法 |
|---|---|---|
| 创建 | 新建 .vue 文件 |
src/components/AppHeader.vue |
| 注册 | 在父组件中 import 引入 |
import AppHeader from './components/AppHeader.vue' |
| 使用 | 在父组件模板中像标签一样写 | <AppHeader /> |
在
<script setup>中import即自动注册,无需额外代码。
2. 父传子:Props
Props 是父组件传递给子组件的"数据包裹",子组件通过 defineProps 声明自己需要接收哪些数据。
2.1 子组件声明接收
vue
<script setup>
// AppHeader.vue
const props = defineProps({
title: {
type: String, // 类型校验
default: '后台管理系统' // 默认值(父组件没传时使用)
},
userInfo: {
type: Object,
default: () => ({ name: '游客', role: '访客' }) // 对象/数组默认值必须用函数返回
}
})
</script>
声明后,模板中直接使用 props 名称 ,无需 .value:{``{ title }}、{``{ userInfo.name }}。
2.2 父组件传值
vue
<!-- 静态传值(不带冒号):直接传字符串 -->
<AppHeader title="静态标题" />
<!-- 动态传值(带冒号):将变量的值传过去 -->
<AppHeader :user-info="userInfo" />
关键 :动态传值必须加冒号 :,否则右边会被当作字符串字面量(如 "userInfo" 这个字符串而不是变量值)。
2.3 命名规则(驼峰 vs 短横线)
- 子组件声明 时使用 camelCase (驼峰):
userInfo - 父组件传递 时使用 kebab-case (短横线):
:user-info - 模板中使用 时使用 camelCase :
{``{ userInfo }} - Vue 会自动进行转换,单单词无需变化(如
title)。
2.4 单向数据流
Props 是只读的:子组件不能直接修改 接收到的 props,否则控制台会警告(target is readonly)。
如果需要改变,应通过 emit 通知父组件修改,或由父组件重新传入新值。
2.5 类型校验与默认值
type:可指定String、Number、Boolean、Array、Object等,传错时控制台会亮警告(不影响运行)。default:基本类型直接写值;对象/数组必须写成 函数,每次返回新对象副本,避免多个组件实例共享同一份数据。
3. 子传父:Emit (自定义事件)
子组件可以通过 emit 向父组件"按门铃"------通知父组件发生了什么事,并可携带参数。
3.1 子组件声明与触发
vue
<script setup>
// AppMenu.vue
const emit = defineEmits(['select-menu']) // 声明事件
function handleClick(menu) {
emit('select-menu', menu) // 触发事件,并将 menu 对象作为参数传出
}
</script>
3.2 父组件监听与处理
vue
<template>
<AppMenu @select-menu="handleSelectMenu" />
</template>
<script setup>
function handleSelectMenu(menu) {
// menu 就是子组件传过来的参数
activeMenu.value = menu.name
}
</script>
事件带参 :emit('事件名', 参数) 的第二个参数即载荷;父组件监听函数直接以参数形式接收。
3.3 事件命名
- 建议使用动词开头 或短横线连接:
logout、select-menu - 三处命名必须完全一致 :
defineEmits(['...'])、emit('...')、@...="",这是最常见的坑。
4. 组件通信模式总结
| 方向 | 机制 | 写法关键词 |
|---|---|---|
| 父 → 子 | Props | 子:defineProps;父::属性="数据" |
| 子 → 父 | Emit | 子:defineEmits + emit('事件');父:@事件="函数" |
核心原则 :"数据在谁手里,逻辑就在谁那里" 。
父组件持有数据和业务逻辑,子组件只负责展现 和上报事件。
5. 组件命名与拆分规范
命名规范:
- 组件文件名:PascalCase + 多单词 (如
AppHeader.vue),避免与原生标签冲突。 - 自定义事件:动词开头、短横线分隔(如
select-menu)。 - 应用级组件建议加统一前缀(如
App),方便识别。
何时拆分组件(三个信号):
- 同一段代码在多个地方重复出现(如多个页面共用同一个头部)
- 单个文件过长,滚动寻代码费劲
- 某块区域的数据和逻辑自成一体,与其它部分无关
6. 常见问题排查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 组件不显示 | import路径错误、文件名大小写不对、模板中未使用标签 | 核对路径、文件名,确认标签已写在模板中 |
| 子组件接收不到数据 | 父组件未传值、动态传值忘加冒号、属性名不一致(驼峰/短横线) | 检查 :user-info 与 userInfo 对应,确保已传 |
| 控制台类型校验警告 | 传入的值类型与 defineProps 声明不符 |
按警告提示修正类型(例如应传对象却传了数字) |
| 子组件修改 props 报警告 | 违反了单向数据流 | 改用 emit 通知父组件修改,或由父组件重新传值 |
| 点击子组件按钮无反应 | 事件名三处不一致(defineEmits/emit/@事件) |
让这三处的名字保持完全一致 |
| 菜单高亮/内容不切换 | 父组件监听函数未更新响应式数据 | 确认函数中修改了对应的 ref/reactive 状态 |
关键代码速查
vue
<!-- 父组件:传递数据 + 监听事件 -->
<AppHeader title="后台管理系统" :user-info="userInfo" @logout="handleLogout" />
<!-- 子组件接收 Props 声明 -->
<script setup>
const props = defineProps({
title: { type: String, default: '默认标题' },
userInfo: { type: Object, default: () => ({ name: '游客' }) }
})
</script>
<!-- 子组件 Emit 声明与触发 -->
<script setup>
const emit = defineEmits(['logout', 'select-menu'])
emit('select-menu', { name: '用户管理' }) // 带参
</script>