大家好!上回我们聊了 Props ------ 爸爸给娃塞零花钱的单向通道。但现实是:孩子也想说话啊!
比如:"爸,我零花钱花完了!"、"妈,帮我关下灯!"......在 Vue 世界里,这种"喊话"机制就是------自定义事件(Custom Events)。
今天,我们就来揭开 defineEmits 和 $emit 的神秘面纱,让你的子组件从此不再"哑巴"!
🗣️ 自定义事件是啥?一句话说清
自定义事件是子组件向父组件传递消息或数据的方式,通过触发事件"通知"父组件:"嘿,我这边有事!"
它和 Props 正好配对:
- Props:父 → 子(传数据)
- 自定义事件:子 → 父(传消息/请求)
合体后,就能实现双向通信,比如经典的"表单输入"、"弹窗关闭"、"列表项删除"等场景。
✨ 基本用法:三步教你"喊话"
第一步:子组件定义并触发事件
在 <script setup> 中,使用 defineEmits 声明你要发射哪些事件(可选但推荐),然后调用它来"喊话"。
html
<!-- ChildButton.vue -->
<script setup>
// 声明这个组件会 emit 哪些事件(类型安全 + 文档化)
const emit = defineEmits(['father', 'mother'])
function callFather() {
// 触发 click 事件,不带参数
emit('father')
}
function callMother() {
// 触发 confirm 事件,并传递数据
emit('mother', { action: 'delete', id: 123 })
}
</script>
<template>
<button @click="callFather">喊爸爸-给钱</button>
<button @click="callMother">喊妈妈-关灯</button>
</template>
💡
defineEmits是宏,无需 import!Vue 3 编译器自动处理。
第二步:父组件监听子组件的事件
在父组件中,像监听原生事件一样,用 @事件名 来接收:
html
<!-- Parent.vue -->
<template>
<ChildButton
@father="giveMoney"
@mother="turnOffLight"
/>
</template>
<script setup>
import ChildButton from './ChildButton.vue'
function giveMoney() {
console.log('给孩子零花钱')
}
function turnOffLight(payload) {
console.log('收到孩子的请求:', payload) // { action: 'delete', id: 123 }
// 这里可以调用 API 删除数据、更新状态等
}
</script>
第三步:跑起来!看控制台输出
点击喊爸爸-给钱和喊妈妈-关灯按钮,你会看到:

完美!父子沟通无障碍!
⚠️ 注意事项:这些雷区别踩!
1. 不要直接修改父组件数据,而是"请求"
❌ 错误示范(试图绕过事件直接改):
js
// 子组件里
props.user.name = '新名字' // 报错!Props 是只读的!
✅ 正确做法:
js
// 子组件
emit('update:name', '新名字')
// 父组件
<UserCard @update:name="name = $event" />
2. 事件名用 kebab-case(短横线)更安全
虽然 Vue 支持驼峰(myEvent),但在模板中强烈建议用短横线命名,避免大小写问题:
vue
<!-- 推荐 -->
<Child @close-dialog="handleClose" />
<!-- 不推荐(某些环境下可能失效) -->
<Child @closeDialog="handleClose" />
对应地,defineEmits 里写成字符串数组即可:
js
defineEmits(['close-dialog'])
3. 事件不是万能的!别滥用
- 如果多个组件都要响应同一个行为(比如全局通知),考虑用 Pinia 。
- 如果只是父子之间简单交互,自定义事件是最轻量、最清晰的选择。
🎯 使用场景:什么时候该"喊话"?
| 场景 | 是否适合用自定义事件 |
|---|---|
| 子组件按钮点击,父组件执行逻辑 | ✅ 经典用法 |
| 表单子组件提交数据 | ✅ emit('submit', formData) |
| 弹窗/抽屉关闭请求 | ✅ emit('close') |
| 列表项被删除 | ✅ emit('delete', itemId) |
| 全局状态变更(如用户登录) | ❌ 用 Pinia 更合适 |
| 兄弟组件通信 | ❌ 通过共同父组件中转,或用状态管理 |
🛠️ 实战示例:一个可关闭的通知卡片
html
<!-- NotificationCard.vue -->
<script setup>
const emit = defineEmits(['close'])
function handleClose() {
emit('close') // 告诉父组件:"我想消失!"
}
</script>
<template>
<div class="notification">
<span>🎉 恭喜你学会自定义事件了!</span>
<button @click="handleClose">×</button>
</div>
</template>
<style scoped>
.notification {
display: flex;
justify-content: space-between;
padding: 12px;
background: #e6f7ff;
border: 1px solid #91d5ff;
}
</style>
父组件使用:
html
<!-- App.vue -->
<template>
<NotificationCard v-if="showNotification" @close="showNotification = false" />
</template>
<script setup>
import { ref } from 'vue'
import NotificationCard from './NotificationCard.vue'
const showNotification = ref(true)
</script>
点击 ×,通知消失!是不是超有成就感?
✅ 总结:自定义事件的黄金法则
- 子喊父听 :子组件用
emit发送,父组件用@监听。 - 命名规范 :事件名用短横线(
close-dialog),别用驼峰。 - 传递数据:可以带任意参数(对象、字符串、数字等)。
- 配合 Props:实现完整的父子双向通信。
自定义事件,就像给孩子装了个对讲机------他不能直接改你钱包,但可以大声喊:"爸!再给我五块钱!"
而你,可以选择给,也可以选择拒绝(比如回一句:"自己赚去!" 😏)。
掌握它,你的 Vue 组件就能真正"活"起来!