$emit与slot作用域插槽核心对比及示例代码
$emit 和作用域插槽的核心对比 + 完整可运行的示例代码,用最清晰的方式帮你区分这两种传值方式,一眼看懂它们的差异和适用场景。
一、核心对比表
| 维度 | this.$emit(子传父事件) |
作用域插槽(Scoped Slot) |
|---|---|---|
| 数据流方向 | 子组件 → 父组件(事件+数据) | 子组件 → 父组件(仅暴露数据) |
| 核心目的 | 子组件触发事件,通知父组件处理逻辑 | 子组件提供数据,父组件自定义渲染内容 |
| 使用方式 | 子组件 $emit('事件名', 数据),父组件 @事件名="方法" |
子组件 <slot :数据名="数据">,父组件 v-slot="{数据名}" |
| 耦合度 | 子组件需提前定义事件,耦合度较高 | 子组件只提供数据,不关心渲染,耦合度极低 |
| 灵活性 | 父组件只能处理逻辑,无法自定义子组件内容 | 父组件可完全自定义子组件内的渲染内容 |
| 适用场景 | 表单提交、弹窗关闭、按钮点击等"触发型"场景 | 表格行操作、列表项自定义渲染等"渲染型"场景 |
二、完整示例对比
下面用同一个表格需求 ,分别写 $emit 版本和作用域插槽版本,可以直接复制运行,直观感受差异。
场景需求
实现一个表格,每行显示"姓名+操作按钮",点击按钮删除对应行数据。
版本1:使用 $emit 实现(耦合度高)
Plain
<!-- 子组件:MyTable-emit.vue -->
<template>
<table border="1" cellpadding="10">
<tr>
<th>姓名</th>
<th>操作</th>
</tr>
<tr v-for="item in data" :key="item.id">
<td>{{ item.name }}</td>
<!-- 子组件固定渲染"删除按钮",并触发事件 -->
<td>
<button @click="$emit('delete', item.id)">删除</button>
</td>
</tr>
</table>
</template>
<script>
export default {
props: {
data: { type: Array, required: true }
}
}
</script>
<!-- 父组件:App.vue -->
<template>
<div>
<h3>$emit 版本(耦合度高)</h3>
<MyTable-emit :data="list" @delete="handleDelete" />
</div>
</template>
<script>
import MyTableEmit from './MyTable-emit.vue'
export default {
components: { MyTableEmit },
data() {
return {
list: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
]
}
},
methods: {
handleDelete(id) {
// 父组件处理删除逻辑
this.list = this.list.filter(item => item.id !== id)
}
}
}
</script>
缺点:如果想把"删除按钮"改成"编辑按钮",必须修改子组件的代码,子组件和业务逻辑强绑定。
版本2:使用作用域插槽实现(灵活性高)
Plain
<!-- 子组件:MyTable-slot.vue -->
<template>
<table border="1" cellpadding="10">
<tr>
<th>姓名</th>
<th>操作</th>
</tr>
<tr v-for="item in data" :key="item.id">
<td>{{ item.name }}</td>
<!-- 子组件只暴露数据,不渲染任何按钮 -->
<td>
<slot :row="item"></slot>
</td>
</tr>
</table>
</template>
<script>
export default {
props: {
data: { type: Array, required: true }
}
}
</script>
<!-- 父组件:App.vue -->
<template>
<div>
<h3>作用域插槽版本(灵活性高)</h3>
<MyTable-slot :data="list">
<!-- 父组件自定义操作按钮,直接使用子组件暴露的 row 数据 -->
<template v-slot="{ row }">
<!-- 想改按钮类型/样式/逻辑,只改父组件即可,子组件无需变动 -->
<button @click="handleDelete(row.id)" style="margin-right: 5px;">删除</button>
<button @click="handleEdit(row)">编辑</button>
</template>
</MyTable-slot>
</div>
</template>
<script>
import MyTableSlot from './MyTable-slot.vue'
export default {
components: { MyTableSlot },
data() {
return {
list: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
]
}
},
methods: {
handleDelete(id) {
this.list = this.list.filter(item => item.id !== id)
},
handleEdit(row) {
alert(`编辑:${row.name}`)
}
}
}
</script>
优点:子组件完全通用,父组件可以自由定义操作按钮(删除、编辑、查看等),甚至修改按钮样式、添加新按钮,无需改动子组件一行代码。
三、总结
-
$emit** 是"事件驱动"**:子组件触发事件,父组件响应处理,适合"触发逻辑"的场景,耦合度较高; -
作用域插槽是"数据暴露":子组件只提供数据,父组件自定义渲染,适合"自定义内容"的场景,灵活性和复用性更高;
-
选型原则 :如果需要父组件自定义子组件内的内容 → 用作用域插槽;如果只是子组件通知父组件做某事 → 用
$emit。