Vue2里的$emit到底怎么用?从基础到实战全解析
做Vue2项目时,组件之间的通信是不是让你头大?尤其是子组件想给父组件传数据、发通知的时候,$emit
就是关键工具!但刚学的时候,是不是经常碰到“事件名写错监听不到”“参数传了但父组件拿不到”这些问题?今天咱们从头梳理 $emit
的用法、避坑点,还有实战场景怎么玩,帮你把组件通信这块儿吃透~
$emit 是干啥的?解决什么场景的问题?
先想清楚组件通信的逻辑:Vue 里组件是分层嵌套的,父组件管子组件,但子组件想主动“告诉”父组件点事儿(比如点击按钮、数据变化),这时候就得用 子传父 的通信方式,$emit
就是专门干这个的!
举个生活例子:你点外卖(子组件是“下单按钮”),点击后得通知商家(父组件是“店铺页面”)做餐,这时候“点击按钮”就是触发 $emit
事件,“商家收到通知做菜”就是父组件监听事件后的操作。
具体技术场景:
- 子组件按钮点击,父组件更新数据(比如列表新增项)
- 子组件表单输入变化,父组件实时校验
- 子组件弹窗关闭,父组件隐藏弹窗
没有 $emit
的话,子组件根本没法主动触发父组件的逻辑,只能等父组件把方法通过 props
传进来(但 props
是父传子,反过来用容易乱,而且不符合设计逻辑),$emit
是官方推荐的子传父通信“正规军”~
基础用法:$emit 咋写?分几步?
核心逻辑就两步:子组件触发事件 → 父组件监听事件
子组件里触发 $emit
语法:this.$emit('事件名', 要传的数据)
- “事件名”:自定义的名字,
'add-item'
'input-change'
,注意 Vue2 里模板中事件名要用短横线命名(kebab-case),因为 HTML 属性不区分大小写,比如你写@myEvent
其实会被解析成@myevent
,所以子组件和父组件的事件名要统一用短横线(如my-event
),避免歧义。 - “要传的数据”:可以是字符串、数字、对象、数组,甚至多个数据(后面讲多参数传递)。
举个简单例子(子组件是 ChildButton.vue
):
<template> <button @click="handleClick">点我通知父组件</button> </template> <script> export default { methods: { handleClick() { // 触发自定义事件,并传递数据(这里传个字符串) this.$emit('btn-click', '我是子组件传来的消息~'); } } } </script>
父组件里监听事件
语法:在子组件标签上用 @事件名="父组件的方法"
,
<template> <div> <!-- 监听子组件的btn-click事件,触发parentMethod --> <ChildButton @btn-click="parentMethod" /> <p>{{ message }}</p> </div> </template> <script> import ChildButton from './ChildButton.vue' export default { components: { ChildButton }, data() { return { message: '' } }, methods: { parentMethod(data) { // data 就是子组件传来的内容 this.message = data; // 把消息显示到页面上 } } } </script>
这样点击子组件按钮,父组件的 parentMethod
就会执行,把数据拿到并更新页面~
传多个参数咋整?父组件咋接收?
子组件想一次性传多个数据,比如同时传“商品ID”和“数量”,咋做?
子组件传多参数
直接在 $emit
里跟多个值:
this.$emit('add-to-cart', productId, quantity); // 传两个参数
父组件接收多参数
有两种方式:
方式1:父组件方法直接接收参数
父组件的方法参数和子组件传的顺序一一对应:
<Child @add-to-cart="handleAdd" /> // ... methods: { handleAdd(productId, quantity) { console.log(productId, quantity); // 子组件传的两个参数 } }
方式2:用 $event 手动指定参数
如果父组件想在绑定事件时,自己加额外参数,就用 $event
代表子组件传来的参数(类似原生事件的 event
对象),比如子组件传了 productId
,父组件想再加个固定值 10
:
<Child @add-to-cart="handleAdd($event, 10)" /> // ... methods: { handleAdd(productId, myNum) { console.log(productId, myNum); // 子组件的productId + 父组件的10 } }
注意:$event
只能对应 子组件传来的第一个参数,如果子组件传了多个,(id, num)
,父组件用 $event
只能拿到 id
,第二个参数得用参数名接收,所以传多参数时,更推荐方式1,逻辑更清晰~
事件名总冲突?咋规范命名?
项目大了,多个组件都用 'click'
'change'
当事件名,父组件监听时根本分不清是哪个子组件触发的!这时候得 给事件名加“作用域”,避免重复。
命名技巧
- 组件前缀法:用组件名当前缀,比如组件叫
UserCard
,事件名可以是user-card-click
user-card-update
。 - 功能模块法:按业务模块分,比如购物车模块的事件叫
cart-add
cart-delete
,订单模块叫order-submit
order-cancel
。 - 语义明确法:事件名要能看出“发生了什么”,
form-validate-pass
(表单验证通过)比form-ok
更清晰。
举个规范的例子(子组件 ProductItem.vue
):
// 触发“商品项点击”事件,传递商品ID this.$emit('product-item-click', productId);
父组件监听:
<ProductItem @product-item-click="handleProductClick" />
这样别人看代码时,一眼就知道是 ProductItem
组件触发的 click
事件,不会和其他组件的 click
搞混~
实战:用 $emit 实现复杂交互(拿购物车举例)
光讲理论太虚,咱们搞个真实场景:子组件是商品卡片,点击“加入购物车”后,父组件更新购物车列表并提示。
步骤1:子组件(ProductCard.vue)触发事件
<template> <div class="product-card"> <h3>{{ product.name }}</h3> <p>价格:{{ product.price }}</p> <button @click="addToCart">加入购物车</button> </div> </template> <script> export default { props: { product: { type: Object, required: true } }, methods: { addToCart() { // 触发addToCart事件,传递商品信息 this.$emit('add-to-cart', this.product); } } } </script>
步骤2:父组件(CartPage.vue)监听并处理
<template> <div> <h2>购物车页面</h2> <div class="product-list"> <!-- 循环渲染商品卡片,每个卡片监听add-to-cart事件 --> <ProductCard v-for="(item, index) in productList" :key="index" :product="item" @add-to-cart="handleAddCart" /> </div> <div class="cart-summary"> <h3>已选商品:</h3> <ul> <li v-for="(item, index) in cartList" :key="index"> {{ item.name }} - ¥{{ item.price }} </li> </ul> </div> </div> </template> <script> import ProductCard from './ProductCard.vue' export default { components: { ProductCard }, data() { return { productList: [ // 假设的商品列表 { name: '手机', price: 3999 }, { name: '耳机', price: 499 } ], cartList: [] // 购物车列表 } }, methods: { handleAddCart(product) { // 把商品加入购物车 this.cartList.push(product); // 提示用户(可以用UI库的Toast,这里模拟alert) alert(`${product.name} 已加入购物车~`); } } } </script>
延伸:双向交互(父组件处理后通知子组件)
如果父组件加完购物车后,要告诉子组件“已加入,按钮变禁用”,咋做?这时候需要 父组件通过 props
传状态,子组件根据状态更新。
-
子组件新增
isAdded
props
,控制按钮是否禁用:<template> <button @click="addToCart" :disabled="isAdded" > {{ isAdded ? '已加入' : '加入购物车' }} </button> </template> <script> export default { props: { product: Object, isAdded: { type: Boolean, default: false } }, // ... } </script>
-
父组件维护每个商品的
isAdded
状态,监听add-to-cart
后更新,并把状态传给子组件:<template> <ProductCard ... :is-added="item.isAdded" /> </template> <script> export default { data() { return { productList: [ { name: '手机', price: 3999, isAdded: false }, { name: '耳机', price: 499, isAdded: false } ], // ... } }, methods: { handleAddCart(product, index) { // 注意这里要传index this.productList[index].isAdded = true; this.cartList.push(product); // ... } } } </script>
这样就实现了 子组件触发事件 → 父组件处理并更新状态 → 父组件通过 props
把状态传回子组件 的完整交互,$emit
在其中负责“子传父”的关键一步~
这些坑你肯定踩过!避坑指南
坑1:事件名大小写/格式写错,监听不到
Vue2 中,父组件模板里的事件名是 区分短横线和驼峰 的!比如子组件用 this.$emit('myEvent')
,父组件写 @my-event
才能监听到(因为 HTML 属性会把驼峰转成小写,@myEvent
实际变成 @myevent
,和子组件的 myEvent
不匹配)。
解决:统一用短横线命名事件,子组件和父组件都写 'my-event'
,避免大小写歧义。
坑2:父组件忘写 @事件名,白触发
子组件里 this.$emit('xxx')
写了,但父组件引用子组件时没加 @xxx="方法"
,那事件永远触发不了。
解决:写代码时,先确定子组件要触发什么事件,父组件立刻加上监听,养成配对写的习惯。
坑3:传对象/数组后,父组件修改影响子组件
子组件传了个对象 this.$emit('send', { name: '张三' })
,父组件拿到后修改 data.name = '李四'
,结果子组件里的原对象也变了(因为引用类型传的是地址)。
解决:如果不想子组件数据被影响,传参时深拷贝,比如用 JSON.parse(JSON.stringify(obj))
或者 lodash 的 cloneDeep
。
坑4:循环里多次触发事件,逻辑重复执行
比如子组件在 v-for
里,每次循环都触发 $emit
,导致父组件方法执行多次。
解决:检查触发条件,比如加 if
判断(只有满足条件才触发),或者用防抖(debounce)控制触发频率。
坑5:混淆 $emit 和 props 的方向
props
是 父传子,$emit
是 子传父,如果搞反了(比如父组件用 $emit
给子组件传数据),逻辑全乱套。
解决:记住方向:父→子用 props
,子→父用 $emit
,实在记不住就想“父是老大,主动给子东西(props
);子是小弟,有事找老大得喊($emit
)”~
和其他通信方式比,$emit 有啥优势?
Vue2 里组件通信方式不少,props
、事件总线(event bus)、Vuex,$emit
适合 组件层级不深、仅需子传父 的场景,优势很明显:
- 对比
props
:props
是父传子,$emit
是子传父,方向互补,官方推荐的“父子通信组合拳”。 - 对比 事件总线:事件总线(比如用
new Vue()
当全局事件中心)容易导致“全局污染”,多个组件乱发事件不好维护;而$emit
是 组件级通信,事件只在父组件和子组件之间传递,范围可控,不容易冲突。 - 对比 Vuex:Vuex 是全局状态管理,适合多个组件跨层级共享数据;但如果只是简单的子传父(比如一个按钮点击),用
$emit
更轻量,不用引入复杂的全局状态管理。
现在再回头看 $emit
,是不是觉得逻辑清晰多了?从基础语法到传参技巧,再到实战和避坑,核心就是“子组件触发事件传数据,父组件监听事件做处理”,记住命名规范、参数传递、场景匹配这几点,组件通信再也不懵~下次写项目时,碰到子传父的需求,直接用 $emit
安排上就完事儿!要是还有疑问,评论区随时喊我,咱们再唠~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。