用最直白、最详细的方式给你讲解子组件传递父组件(Emit事件)!
📞 Emit:子组件给父组件打电话
🎭 最简单的比喻
想象你是儿子 (子组件),你的爸爸(父组件)在楼下:
👦 儿子(子组件)在楼上
↓ 喊话:"爸爸,我饿了!"
👨 爸爸(父组件)在楼下
↓ 听到后去做饭
📱 第一步:最简单的例子
子组件:发送消息(打电话)
vue
<!-- Son.vue(儿子) -->
<template>
<div>
<button @click="callDad">我饿了,叫爸爸</button>
</div>
</template>
<script>
export default {
methods: {
callDad() {
// 📞 给爸爸打电话(触发事件)
this.$emit('hungry')
console.log('儿子:爸爸,我饿了!')
}
}
}
</script>
父组件:接收消息(接电话)
vue
<!-- App.vue(爸爸) -->
<template>
<div>
<!-- 📞 监听儿子的电话 -->
<Son @hungry="makeDinner" />
</div>
</template>
<script>
import Son from './Son.vue'
export default {
components: { Son },
methods: {
makeDinner() {
console.log('爸爸:好的,我去做饭!')
alert('爸爸开始做饭了')
}
}
}
</script>
流程:
- 儿子点击按钮
- 儿子调用
this.$emit('hungry')(打电话) - 爸爸监听到
@hungry事件(接电话) - 爸爸执行
makeDinner方法(去做饭)
📦 第二步:传递数据
子组件:打电话时说具体内容
vue
<!-- Son.vue -->
<template>
<div>
<button @click="orderFood('汉堡')">我想吃汉堡</button>
<button @click="orderFood('披萨')">我想吃披萨</button>
<button @click="orderFood('炸鸡')">我想吃炸鸡</button>
</div>
</template>
<script>
export default {
methods: {
orderFood(foodName) {
// 📞 打电话并告诉爸爸想吃什么
this.$emit('order', foodName)
console.log(`儿子:爸爸,我想吃${foodName}!`)
}
}
}
</script>
父组件:接电话并知道具体内容
vue
<!-- App.vue -->
<template>
<div>
<h2>今天吃:{{ todayFood }}</h2>
<!-- 📞 监听order事件,接收foodName参数 -->
<Son @order="handleOrder" />
</div>
</template>
<script>
import Son from './Son.vue'
export default {
components: { Son },
data() {
return {
todayFood: ''
}
},
methods: {
handleOrder(foodName) {
console.log(`爸爸:好的,我去买${foodName}!`)
this.todayFood = foodName
}
}
}
</script>
流程:
- 儿子点击"我想吃汉堡"
- 儿子调用
this.$emit('order', '汉堡') - 爸爸的
handleOrder方法接收到参数'汉堡' - 爸爸更新
todayFood = '汉堡'
🎯 第三步:传递多个参数
vue
<!-- Son.vue -->
<template>
<div>
<button @click="orderMeal">点餐</button>
</div>
</template>
<script>
export default {
methods: {
orderMeal() {
// 📞 可以传递多个参数
this.$emit('order', '汉堡', 2, '加辣')
// 事件名 参数1 参数2 参数3
}
}
}
</script>
vue
<!-- App.vue -->
<template>
<Son @order="handleOrder" />
</template>
<script>
export default {
methods: {
handleOrder(food, quantity, note) {
console.log(`食物:${food}`) // "汉堡"
console.log(`数量:${quantity}`) // 2
console.log(`备注:${note}`) // "加辣"
}
}
}
</script>
🔧 第四步:声明Emits(推荐写法)
为什么要声明?
就像打电话前要告诉别人"我可能会给你打这些类型的电话"
vue
<!-- Son.vue -->
<script>
export default {
// 📋 声明:我可能会触发这些事件
emits: ['hungry', 'order', 'sleep'],
methods: {
callDad() {
this.$emit('hungry')
},
orderFood(food) {
this.$emit('order', food)
},
goToSleep() {
this.$emit('sleep')
}
}
}
</script>
带验证的声明
vue
<script>
export default {
// 🔍 对象形式:可以验证参数
emits: {
// 简单声明
hungry: null,
// 带验证
order(food) {
if (!food) {
console.warn('必须告诉我想吃什么!')
return false
}
return true
},
// 验证多个参数
addToCart(product, quantity) {
if (quantity <= 0) {
console.warn('数量必须大于0!')
return false
}
return true
}
}
}
</script>
🎪 完整实战案例:计数器
子组件:操作按钮
vue
<!-- CounterOperation.vue -->
<template>
<div class="counter-operations">
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<button @click="addTen">+10</button>
<button @click="reset">重置</button>
</div>
</template>
<script>
export default {
// 📋 声明可以触发的事件
emits: ['add', 'sub', 'addN', 'reset'],
methods: {
increment() {
// 📞 通知父组件:加1
this.$emit('add')
},
decrement() {
// 📞 通知父组件:减1
this.$emit('sub')
},
addTen() {
// 📞 通知父组件:加10
this.$emit('addN', 10)
},
reset() {
// 📞 通知父组件:重置
this.$emit('reset')
}
}
}
</script>
<style scoped>
button {
margin: 5px;
padding: 10px 20px;
font-size: 16px;
}
</style>
父组件:处理事件
vue
<!-- App.vue -->
<template>
<div class="app">
<h1>计数器</h1>
<h2>当前计数:{{ counter }}</h2>
<!-- 📞 监听子组件的所有事件 -->
<CounterOperation
@add="addOne"
@sub="subOne"
@addN="addNNum"
@reset="resetCounter"
/>
</div>
</template>
<script>
import CounterOperation from './CounterOperation.vue'
export default {
components: { CounterOperation },
data() {
return {
counter: 0
}
},
methods: {
addOne() {
this.counter++
console.log('加1,当前:', this.counter)
},
subOne() {
this.counter--
console.log('减1,当前:', this.counter)
},
addNNum(num) {
this.counter += num
console.log(`加${num},当前:`, this.counter)
},
resetCounter() {
this.counter = 0
console.log('重置为0')
}
}
}
</script>
🎯 实战案例2:商品列表
子组件:商品卡片
vue
<!-- ProductCard.vue -->
<template>
<div class="product-card">
<img :src="product.image" :alt="product.name">
<h3>{{ product.name }}</h3>
<p class="price">¥{{ product.price }}</p>
<div class="actions">
<!-- 📞 点击时通知父组件 -->
<button @click="handleAddToCart">加入购物车</button>
<button @click="handleBuyNow">立即购买</button>
</div>
</div>
</template>
<script>
export default {
props: {
product: {
type: Object,
required: true
}
},
// 📋 声明事件
emits: ['add-to-cart', 'buy-now'],
methods: {
handleAddToCart() {
// 📞 通知父组件:加入购物车
this.$emit('add-to-cart', this.product)
console.log('子组件:通知父组件加入购物车', this.product.name)
},
handleBuyNow() {
// 📞 通知父组件:立即购买
this.$emit('buy-now', this.product)
console.log('子组件:通知父组件立即购买', this.product.name)
}
}
}
</script>
父组件:处理购物逻辑
vue
<!-- App.vue -->
<template>
<div class="app">
<h1>商品列表</h1>
<div class="product-list">
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
@add-to-cart="addToCart"
@buy-now="buyNow"
/>
</div>
<div class="cart">
<h2>购物车({{ cartCount }}件)</h2>
</div>
</div>
</template>
<script>
import ProductCard from './ProductCard.vue'
export default {
components: { ProductCard },
data() {
return {
products: [
{ id: 1, name: 'iPhone 15', price: 5999, image: 'iphone.jpg' },
{ id: 2, name: 'MacBook Pro', price: 12999, image: 'macbook.jpg' },
{ id: 3, name: 'AirPods', price: 1299, image: 'airpods.jpg' }
],
cart: []
}
},
computed: {
cartCount() {
return this.cart.length
}
},
methods: {
addToCart(product) {
console.log('父组件:收到加入购物车请求', product.name)
this.cart.push(product)
alert(`${product.name} 已加入购物车!`)
},
buyNow(product) {
console.log('父组件:收到立即购买请求', product.name)
alert(`正在购买 ${product.name},价格:¥${product.price}`)
}
}
}
</script>
🎭 Emit的两种写法对比
写法1:在模板中直接写表达式
vue
<!-- 子组件 -->
<template>
<!-- 直接在模板中触发事件 -->
<button @click="$emit('add')">+1</button>
<button @click="$emit('addN', 10)">+10</button>
</template>
<script>
export default {
emits: ['add', 'addN']
}
</script>
写法2:在methods中触发(推荐)
vue
<!-- 子组件 -->
<template>
<button @click="handleAdd">+1</button>
<button @click="handleAddTen">+10</button>
</template>
<script>
export default {
emits: ['add', 'addN'],
methods: {
handleAdd() {
// 可以在这里做一些逻辑处理
console.log('准备加1')
this.$emit('add')
},
handleAddTen() {
// 可以在这里做一些逻辑处理
console.log('准备加10')
this.$emit('addN', 10)
}
}
}
</script>
🔍 深入理解:事件流程
完整流程图
1. 用户点击子组件的按钮
↓
2. 子组件执行方法
↓
3. 方法中调用 this.$emit('事件名', 参数)
↓
4. Vue检测到emit
↓
5. Vue查找父组件中是否监听了这个事件(@事件名)
↓
6. 如果有监听,执行父组件的方法
↓
7. 父组件方法接收参数并处理
代码流程
vue
<!-- 子组件 -->
<template>
<button @click="sendMessage">发送</button>
</template>
<script>
export default {
methods: {
sendMessage() {
// 1️⃣ 子组件触发事件
this.$emit('message', 'Hello')
console.log('1. 子组件:发送消息')
}
}
}
</script>
vue
<!-- 父组件 -->
<template>
<Child @message="receiveMessage" />
</template>
<script>
export default {
methods: {
receiveMessage(msg) {
// 2️⃣ 父组件接收事件
console.log('2. 父组件:收到消息', msg)
}
}
}
</script>
控制台输出:
1. 子组件:发送消息
2. 父组件:收到消息 Hello
⚠️ 注意事项
🚫 错误1:事件名大小写
vue
<!-- ❌ 错误:使用驼峰命名 -->
<Child @addToCart="handleAdd" />
<!-- 子组件 -->
this.$emit('addToCart') // 可能不工作
<!-- ✅ 正确:使用kebab-case -->
<Child @add-to-cart="handleAdd" />
<!-- 子组件 -->
this.$emit('add-to-cart') // 推荐
🚫 错误2:忘记在父组件监听
vue
<!-- ❌ 错误:子组件emit了,但父组件没监听 -->
<Child /> <!-- 没有@事件名 -->
<!-- 子组件 -->
this.$emit('message') // 发送了,但没人接收
<!-- ✅ 正确:父组件要监听 -->
<Child @message="handleMessage" />
🚫 错误3:参数接收错误
vue
<!-- 子组件发送多个参数 -->
this.$emit('order', '汉堡', 2, '加辣')
<!-- ❌ 错误:只接收一个参数 -->
methods: {
handleOrder(food) {
// 只能拿到'汉堡',拿不到2和'加辣'
}
}
<!-- ✅ 正确:接收所有参数 -->
methods: {
handleOrder(food, quantity, note) {
console.log(food, quantity, note)
}
}
💡 记忆口诀
子传父用emit,
打电话通知你。
父组件要监听,
@符号加事件名。
参数可以传多个,
父组件按顺序收。
🎯 总结
Emit就像打电话
| 步骤 | 子组件(打电话) | 父组件(接电话) |
|---|---|---|
| 1 | 声明emits | 准备监听 @事件名 |
| 2 | 触发 this.$emit('事件名', 参数) | 定义处理方法 |
| 3 | 发送消息和数据 | 接收消息和数据 |
| 4 | 完成通知 | 处理业务逻辑 |
关键点
- 子组件 :用
this.$emit('事件名', 参数)发送消息 - 父组件 :用
@事件名="方法"监听消息 - 参数传递:可以传递多个参数
- 事件命名:推荐使用kebab-case(短横线命名)
- 声明emits:推荐在子组件中声明可触发的事件
现在明白了吗?子传父就是子组件通过emit"打电话"通知父组件,父组件监听"接电话"并处理!😊