qidao123.com技术社区-IT企服评测·应用市场
标题:
Vue3 组件通讯与插槽
[打印本页]
作者:
圆咕噜咕噜
时间:
2025-4-28 07:59
标题:
Vue3 组件通讯与插槽
Vue3 组件通讯方式全解(10种方案)
一、组件通讯方式概览
通讯方式
适用场景
数据流向
复杂度
Props/自界说事件父子组件简朴通讯父 ↔ 子⭐v-model 双向绑定父子表单组件父 ↔ 子⭐⭐Provide/Inject跨层级组件通讯祖先 → 子女⭐⭐事件总线恣意组件间通讯恣意方向⭐⭐⭐模板引用(ref)父操作子组件父 → 子⭐Pinia 状态管理复杂应用状态共享全局共享⭐⭐⭐欣赏器存储持久化数据共享全局共享⭐⭐⭐attrs/attrs/listeners透传属性/事件父 → 深层子组件⭐⭐作用域插槽子向父通报渲染内容子 → 父⭐⭐路由参数页面间数据通报页面间⭐
二、焦点通讯方案详解
1. Props / 自界说事件(父子通讯)
<!-- 父组件 Parent.vue -->
<template>
<Child
:message="parentMsg"
@update="handleUpdate"
/>
</template>
<script setup>
import { ref } from 'vue'
const parentMsg = ref('Hello from parent')
const handleUpdate = (newMsg) => {
parentMsg.value = newMsg
}
</script>
<!-- 子组件 Child.vue -->
<script setup>
defineProps(['message'])
const emit = defineEmits(['update'])
const sendToParent = () => {
emit('update', 'New message from child')
}
</script>
复制代码
2. v-model 双向绑定(表单场景)
<!-- 父组件 -->
<CustomInput v-model="username" />
<!-- 子组件 CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
复制代码
3. Provide/Inject(跨层级)
// 祖先组件
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', {
theme,
toggleTheme: () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
})
// 后代组件
import { inject } from 'vue'
const { theme, toggleTheme } = inject('theme')
复制代码
4. 事件总线 mitt(恣意组件)
// eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
// 组件A(发送)
emitter.emit('global-event', { data: 123 })
// 组件B(接收)
emitter.on('global-event', (data) => {
console.log(data)
})
复制代码
5. Pinia 状态管理(复杂应用)
// stores/user.js
export const useUserStore = defineStore('user', {
state: () => ({ name: 'Alice' }),
actions: {
updateName(newName) {
this.name = newName
}
}
})
// 组件使用
const userStore = useUserStore()
userStore.updateName('Bob')
复制代码
6. 作用域插槽(子传父)
<!-- 子组件 -->
<template>
<slot :data="childData" />
</template>
<script setup>
const childData = { message: 'From child' }
</script>
<!-- 父组件 -->
<Child>
<template #default="{ data }">
{{ data.message }}
</template>
</Child>
复制代码
三、进阶通讯本领
1. 多层属性透传
// 父组件
<GrandParent>
<Parent>
<Child />
</Parent>
</GrandParent>
// GrandParent.vue
<template>
<Parent v-bind="$attrs">
<slot />
</Parent>
</template>
// 使用 inheritAttrs: false 关闭自动继承
复制代码
2. 动态组件通讯
<component
:is="currentComponent"
@custom-event="handleEvent"
/>
复制代码
3. 自界说 Hook 封装
// hooks/useCounter.js
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
return { count, increment }
}
// 组件A
const { count: countA } = useCounter()
// 组件B
const { count: countB } = useCounter(10)
复制代码
4. 全局事件总线加强版
// eventBus.ts
type EventMap = {
'user-login': { userId: string }
'cart-update': CartItem[]
}
export const emitter = mitt<EventMap>()
// 安全使用
emitter.emit('user-login', { userId: '123' })
复制代码
四、最佳实践指南
简朴场景优先方案
父子通讯:Props + 自界说事件
表单组件:v-model
简朴共享数据:Provide/Inject
复杂应用推荐方案
全局状态管理:Pinia
跨组件通讯:事件总线
持久化数据:localStorage + Pinia 插件
性能优化本领
避免在 Props 中通报大型对象
使用 computed 属性优化渲染
对频繁触发的事件进行防抖处理
及时清理事件监听器
TypeScript 加强
// Props 类型定义
defineProps<{
title: string
data: number[]
}>()
// 事件类型定义
defineEmits<{
(e: 'update', value: string): void
(e: 'delete', id: number): void
}>()
复制代码
五、常见题目解决方案
Q1: 如何避免 Props 层层通报?
✅ 使用 Provide/Inject 或 Pinia
Q2: 子组件如何修改父组件数据?
✅ 通过自界说事件关照父组件修改
Q3: 如何实现兄弟组件通讯?
✅ 方案1:通过共同的父组件中转
✅ 方案2:使用事件总线或 Pinia
Q4: 如何包管响应式数据安全?
✅ 使用 readonly 限定修改权限:
provide('config', readonly(config))
复制代码
Q5: 如何实现跨路由组件通讯?
✅ 使用 Pinia 状态管理
✅ 通过路由参数通报
✅ 使用 localStorage 持久化存储
插槽详解
一、插槽焦点概念
1. 插槽作用
答应父组件向子组件
插入自界说内容
,实现组件的高度可定制化
2. 组件通讯对比
通讯方式
数据流向
内容类型
Props父 → 子纯数据插槽父 → 子模板/组件/HTML片断
二、基础插槽类型
1. 默认插槽
子组件
:
<!-- ChildComponent.vue -->
<template>
<div class="card">
<slot>默认内容(父组件未提供时显示)</slot>
</div>
</template>
复制代码
父组件
:
<ChildComponent>
<p>自定义卡片内容</p>
</ChildComponent>
复制代码
2. 具名插槽
子组件
:
<template>
<div class="layout">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
复制代码
父组件
:
<ChildComponent>
<template #header>
<h1>页面标题</h1>
</template>
<p>主内容区域</p>
<template v-slot:footer>
<p>版权信息 © 2023</p>
</template>
</ChildComponent>
复制代码
三、作用域插槽(焦点进阶)
1. 数据通报原理
子组件向插槽通报数据 → 父组件吸收使用
子组件
:
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="index"></slot>
</li>
</ul>
</template>
<script setup>
const items = ref([{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }])
</script>
复制代码
父组件
:
<ChildComponent>
<template #default="{ item, index }">
<span>{{ index + 1 }}. {{ item.name }}</span>
<button @click="deleteItem(item.id)">删除</button>
</template>
</ChildComponent>
复制代码
2. 应用场景示例
场景
:表格组件支持自界说列渲染
子组件
:
<template>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">
{{ col.title }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td v-for="col in columns" :key="col.key">
<slot :name="col.key" :row="row">
{{ row[col.key] }} <!-- 默认显示 -->
</slot>
</td>
</tr>
</tbody>
</table>
</template>
复制代码
父组件
:
<DataTable :data="users" :columns="columns">
<template #action="{ row }">
<button @click="editUser(row.id)">编辑</button>
<button @click="deleteUser(row.id)">删除</button>
</template>
<template #status="{ row }">
<span :class="row.status">{{ row.status | statusText }}</span>
</template>
</DataTable>
复制代码
四、动态插槽名
子组件
:
<template>
<div class="dynamic-slot">
<slot :name="slotName"></slot>
</div>
</template>
<script setup>
defineProps({
slotName: {
type: String,
default: 'default'
}
})
</script>
复制代码
父组件
:
<ChildComponent :slotName="currentSlot">
<template #[currentSlot]>
<p>动态插槽内容(当前使用 {{ currentSlot }} 插槽)</p>
</template>
</ChildComponent>
复制代码
五、高级本领
1. 插槽继续($slots)
访问子组件插槽内容:
// 子组件内部
const slots = useSlots()
console.log(slots.header()) // 获取具名插槽内容
复制代码
2. 渲染函数中使用插槽
// 使用 h() 函数创建元素
export default {
render() {
return h('div', [
this.$slots.default?.() || '默认内容',
h('div', { class: 'footer' }, this.$slots.footer?.())
])
}
}
复制代码
六、最佳实践指南
定名规范
使用小写字母 + 连字符定名具名插槽(如 #user-avatar)
避免使用保存字作为插槽名(如 default、item)
性能优化
<!-- 通过 v-if 控制插槽内容渲染 -->
<template #header v-if="showHeader">
<HeavyComponent />
</template>
复制代码
类型安全(TypeScript)
// 定义作用域插槽类型
defineSlots<{
default?: (props: { item: T; index: number }) => any
header?: () => any
footer?: () => any
}>()
复制代码
七、常见题目解答
Q1:如何强制要求必须提供某个插槽
// 子组件中验证
export default {
mounted() {
if (!this.$slots.header) {
console.error('必须提供 header 插槽内容')
}
}
}
复制代码
Q2:插槽内容如何访问子组件方法?
<!-- 子组件暴露方法 -->
<slot :doSomething="handleAction"></slot>
<!-- 父组件使用 -->
<template #default="{ doSomething }">
<button @click="doSomething">触发子组件方法</button>
</template>
复制代码
Q3:如何实现插槽内容过渡动画?
<transition name="fade" mode="out-in">
<slot></slot>
</transition>
复制代码
八、综合应用案例
可设置的模态框组件
<!-- Modal.vue -->
<template>
<div class="modal" v-show="visible">
<div class="modal-header">
<slot name="header">
<h2>{{ title }}</h2>
<button @click="close">×</button>
</slot>
</div>
<div class="modal-body">
<slot :close="close"></slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button @click="close">关闭</button>
</slot>
</div>
</div>
</template>
复制代码
父组件使用
:
<Modal v-model:visible="showModal" title="自定义标题">
<template #header>
<h1 style="color: red;">紧急通知</h1>
</template>
<template #default="{ close }">
<p>确认删除此项?</p>
<button @click="confirmDelete; close()">确认</button>
</template>
<template #footer>
<button @click="showModal = false">取消</button>
</template>
</Modal>
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 qidao123.com技术社区-IT企服评测·应用市场 (https://dis.qidao123.com/)
Powered by Discuz! X3.4