当前位置: 首页 > news >正文

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' })

四、最佳实践指南

  1. 简单场景优先方案

    • 父子通信:Props + 自定义事件

    • 表单组件:v-model

    • 简单共享数据:Provide/Inject

  2. 复杂应用推荐方案

    • 全局状态管理:Pinia

    • 跨组件通信:事件总线

    • 持久化数据:localStorage + Pinia 插件

  3. 性能优化技巧

    • 避免在 Props 中传递大型对象

    • 使用 computed 属性优化渲染

    • 对频繁触发的事件进行防抖处理

    • 及时清理事件监听器

  4. 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?.())])}
}

六、最佳实践指南

  1. 命名规范

    • 使用小写字母 + 连字符命名具名插槽(如 #user-avatar

    • 避免使用保留字作为插槽名(如 defaultitem

  2. 性能优化

    <!-- 通过 v-if 控制插槽内容渲染 -->
    <template #header v-if="showHeader"><HeavyComponent />
    </template>
  3. 类型安全(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>

相关文章:

  • Qt开发:如何加载样式文件
  • 玩转Pygame绘图:从简单图形到炫酷精灵
  • 【开源飞控】调试
  • maven打包时配置多环境参数
  • 设置右键打开VSCode
  • MCP协议:AI与数据世界的“万能连接器“
  • [创业之路-390]:人力资源 - 社会性生命系统的解构与重构:人的角色嬗变与组织进化论
  • SpringBoot技术概述与应用实践
  • GPT系列模型-20250426
  • 《软件设计师》复习笔记(6.1)——信息安全及技术
  • 常见的机器视觉通用软件
  • 数据安全和合规性市场分析
  • Redis常见面试题——List对象
  • Redis 数据分片三大方案深度解析与 Java 实战
  • Python爬虫实战:获取高考资源网各学科精品复习资料
  • 蓝桥杯 8. 移动距离
  • Angular开发经常涉及到组件间传递参数,用原生js开发时,如何解决这些问题?
  • 使用 Spring Boot 进行开发
  • 印刷设备管理绩效考核制度与设备优化路径
  • Linux: 如何在VMware上安装Ubuntu操作系统
  • 俄罗斯称已收复库尔斯克州
  • 重新认识中国女性|婚姻,古代传统家庭再生产的根本之道
  • “80后”王建浩履新三沙市委常委、组织部部长、秘书长
  • 复旦大学校长金力:将配套出台多项政策推动科技成果转化
  • 韩国检方以受贿嫌疑起诉前总统文在寅
  • 吃菜和吃肉,哪个更“增肌”?