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: stringdata: 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 }) => anyheader?: () => anyfooter?: () => 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>