Vue3 组件通讯与插槽

打印 上一主题 下一主题

主题 1623|帖子 1623|积分 4869

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
Vue3 组件通讯方式全解(10种方案)


一、组件通讯方式概览

通讯方式适用场景数据流向复杂度Props/自界说事件父子组件简朴通讯父 ↔ 子⭐v-model 双向绑定父子表单组件父 ↔ 子⭐⭐Provide/Inject跨层级组件通讯祖先 → 子女⭐⭐事件总线恣意组件间通讯恣意方向⭐⭐⭐模板引用(ref)父操作子组件父 → 子⭐Pinia 状态管理复杂应用状态共享全局共享⭐⭐⭐欣赏器存储持久化数据共享全局共享⭐⭐⭐attrs/attrs/listeners透传属性/事件父 → 深层子组件⭐⭐作用域插槽子向父通报渲染内容子 → 父⭐⭐路由参数页面间数据通报页面间⭐
二、焦点通讯方案详解

1. Props / 自界说事件(父子通讯)

  1. <!-- 父组件 Parent.vue -->
  2. <template>
  3.   <Child
  4.     :message="parentMsg"
  5.     @update="handleUpdate"
  6.   />
  7. </template>
  8. <script setup>
  9. import { ref } from 'vue'
  10. const parentMsg = ref('Hello from parent')
  11. const handleUpdate = (newMsg) => {
  12.   parentMsg.value = newMsg
  13. }
  14. </script>
  15. <!-- 子组件 Child.vue -->
  16. <script setup>
  17. defineProps(['message'])
  18. const emit = defineEmits(['update'])
  19. const sendToParent = () => {
  20.   emit('update', 'New message from child')
  21. }
  22. </script>
复制代码
2. v-model 双向绑定(表单场景)

  1. <!-- 父组件 -->
  2. <CustomInput v-model="username" />
  3. <!-- 子组件 CustomInput.vue -->
  4. <template>
  5.   <input
  6.     :value="modelValue"
  7.     @input="$emit('update:modelValue', $event.target.value)"
  8.   >
  9. </template>
  10. <script setup>
  11. defineProps(['modelValue'])
  12. defineEmits(['update:modelValue'])
  13. </script>
复制代码
3. Provide/Inject(跨层级)

  1. // 祖先组件
  2. import { provide, ref } from 'vue'
  3. const theme = ref('dark')
  4. provide('theme', {
  5.   theme,
  6.   toggleTheme: () => {
  7.     theme.value = theme.value === 'dark' ? 'light' : 'dark'
  8.   }
  9. })
  10. // 后代组件
  11. import { inject } from 'vue'
  12. const { theme, toggleTheme } = inject('theme')
复制代码
4. 事件总线 mitt(恣意组件)

  1. // eventBus.js
  2. import mitt from 'mitt'
  3. export const emitter = mitt()
  4. // 组件A(发送)
  5. emitter.emit('global-event', { data: 123 })
  6. // 组件B(接收)
  7. emitter.on('global-event', (data) => {
  8.   console.log(data)
  9. })
复制代码
5. Pinia 状态管理(复杂应用)

  1. // stores/user.js
  2. export const useUserStore = defineStore('user', {
  3.   state: () => ({ name: 'Alice' }),
  4.   actions: {
  5.     updateName(newName) {
  6.       this.name = newName
  7.     }
  8.   }
  9. })
  10. // 组件使用
  11. const userStore = useUserStore()
  12. userStore.updateName('Bob')
复制代码
6. 作用域插槽(子传父)

  1. <!-- 子组件 -->
  2. <template>
  3.   <slot :data="childData" />
  4. </template>
  5. <script setup>
  6. const childData = { message: 'From child' }
  7. </script>
  8. <!-- 父组件 -->
  9. <Child>
  10.   <template #default="{ data }">
  11.     {{ data.message }}
  12.   </template>
  13. </Child>
复制代码

三、进阶通讯本领

1. 多层属性透传

  1. // 父组件
  2. <GrandParent>
  3.   <Parent>
  4.     <Child />
  5.   </Parent>
  6. </GrandParent>
  7. // GrandParent.vue
  8. <template>
  9.   <Parent v-bind="$attrs">
  10.     <slot />
  11.   </Parent>
  12. </template>
  13. // 使用 inheritAttrs: false 关闭自动继承
复制代码
2. 动态组件通讯

  1. <component
  2.   :is="currentComponent"
  3.   @custom-event="handleEvent"
  4. />
复制代码
3. 自界说 Hook 封装

  1. // hooks/useCounter.js
  2. export function useCounter(initial = 0) {
  3.   const count = ref(initial)
  4.   
  5.   const increment = () => count.value++
  6.   
  7.   return { count, increment }
  8. }
  9. // 组件A
  10. const { count: countA } = useCounter()
  11. // 组件B
  12. const { count: countB } = useCounter(10)
复制代码
4. 全局事件总线加强版

  1. // eventBus.ts
  2. type EventMap = {
  3.   'user-login': { userId: string }
  4.   'cart-update': CartItem[]
  5. }
  6. export const emitter = mitt<EventMap>()
  7. // 安全使用
  8. emitter.emit('user-login', { userId: '123' })
复制代码

四、最佳实践指南


  • 简朴场景优先方案

    • 父子通讯:Props + 自界说事件
    • 表单组件:v-model
    • 简朴共享数据:Provide/Inject

  • 复杂应用推荐方案

    • 全局状态管理:Pinia
    • 跨组件通讯:事件总线
    • 持久化数据:localStorage + Pinia 插件

  • 性能优化本领

    • 避免在 Props 中通报大型对象
    • 使用 computed 属性优化渲染
    • 对频繁触发的事件进行防抖处理
    • 及时清理事件监听器

  • TypeScript 加强
  1. // Props 类型定义
  2. defineProps<{
  3.   title: string
  4.   data: number[]
  5. }>()
  6. // 事件类型定义
  7. defineEmits<{
  8.   (e: 'update', value: string): void
  9.   (e: 'delete', id: number): void
  10. }>()
复制代码

五、常见题目解决方案

Q1: 如何避免 Props 层层通报?
✅ 使用 Provide/Inject 或 Pinia
Q2: 子组件如何修改父组件数据?
✅ 通过自界说事件关照父组件修改
Q3: 如何实现兄弟组件通讯?
✅ 方案1:通过共同的父组件中转
✅ 方案2:使用事件总线或 Pinia
Q4: 如何包管响应式数据安全?
✅ 使用 readonly 限定修改权限:
  1. provide('config', readonly(config))
复制代码
Q5: 如何实现跨路由组件通讯?
✅ 使用 Pinia 状态管理
✅ 通过路由参数通报
✅ 使用 localStorage 持久化存储

   
插槽详解


一、插槽焦点概念

1. 插槽作用

答应父组件向子组件插入自界说内容,实现组件的高度可定制化
2. 组件通讯对比

通讯方式数据流向内容类型Props父 → 子纯数据插槽父 → 子模板/组件/HTML片断
二、基础插槽类型

1. 默认插槽

子组件
  1. <!-- ChildComponent.vue -->
  2. <template>
  3.   <div class="card">
  4.     <slot>默认内容(父组件未提供时显示)</slot>
  5.   </div>
  6. </template>
复制代码
父组件
  1. <ChildComponent>
  2.   <p>自定义卡片内容</p>
  3. </ChildComponent>
复制代码
2. 具名插槽

子组件
  1. <template>
  2.   <div class="layout">
  3.     <header>
  4.       <slot name="header"></slot>
  5.     </header>
  6.     <main>
  7.       <slot></slot> <!-- 默认插槽 -->
  8.     </main>
  9.     <footer>
  10.       <slot name="footer"></slot>
  11.     </footer>
  12.   </div>
  13. </template>
复制代码
父组件
  1. <ChildComponent>
  2.   <template #header>
  3.     <h1>页面标题</h1>
  4.   </template>
  5.   <p>主内容区域</p>
  6.   <template v-slot:footer>
  7.     <p>版权信息 © 2023</p>
  8.   </template>
  9. </ChildComponent>
复制代码

三、作用域插槽(焦点进阶)

1. 数据通报原理

子组件向插槽通报数据 → 父组件吸收使用
子组件
  1. <template>
  2.   <ul>
  3.     <li v-for="item in items" :key="item.id">
  4.       <slot :item="item" :index="index"></slot>
  5.     </li>
  6.   </ul>
  7. </template>
  8. <script setup>
  9. const items = ref([{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }])
  10. </script>
复制代码
父组件
  1. <ChildComponent>
  2.   <template #default="{ item, index }">
  3.     <span>{{ index + 1 }}. {{ item.name }}</span>
  4.     <button @click="deleteItem(item.id)">删除</button>
  5.   </template>
  6. </ChildComponent>
复制代码
2. 应用场景示例

场景:表格组件支持自界说列渲染
子组件
  1. <template>
  2.   <table>
  3.     <thead>
  4.       <tr>
  5.         <th v-for="col in columns" :key="col.key">
  6.           {{ col.title }}
  7.         </th>
  8.       </tr>
  9.     </thead>
  10.     <tbody>
  11.       <tr v-for="row in data" :key="row.id">
  12.         <td v-for="col in columns" :key="col.key">
  13.           <slot :name="col.key" :row="row">
  14.             {{ row[col.key] }} <!-- 默认显示 -->
  15.           </slot>
  16.         </td>
  17.       </tr>
  18.     </tbody>
  19.   </table>
  20. </template>
复制代码
父组件
  1. <DataTable :data="users" :columns="columns">
  2.   <template #action="{ row }">
  3.     <button @click="editUser(row.id)">编辑</button>
  4.     <button @click="deleteUser(row.id)">删除</button>
  5.   </template>
  6.   <template #status="{ row }">
  7.     <span :class="row.status">{{ row.status | statusText }}</span>
  8.   </template>
  9. </DataTable>
复制代码

四、动态插槽名

子组件
  1. <template>
  2.   <div class="dynamic-slot">
  3.     <slot :name="slotName"></slot>
  4.   </div>
  5. </template>
  6. <script setup>
  7. defineProps({
  8.   slotName: {
  9.     type: String,
  10.     default: 'default'
  11.   }
  12. })
  13. </script>
复制代码
父组件
  1. <ChildComponent :slotName="currentSlot">
  2.   <template #[currentSlot]>
  3.     <p>动态插槽内容(当前使用 {{ currentSlot }} 插槽)</p>
  4.   </template>
  5. </ChildComponent>
复制代码

五、高级本领

1. 插槽继续($slots)

访问子组件插槽内容:
  1. // 子组件内部
  2. const slots = useSlots()
  3. console.log(slots.header()) // 获取具名插槽内容
复制代码
2. 渲染函数中使用插槽

  1. // 使用 h() 函数创建元素
  2. export default {
  3.   render() {
  4.     return h('div', [
  5.       this.$slots.default?.() || '默认内容',
  6.       h('div', { class: 'footer' }, this.$slots.footer?.())
  7.     ])
  8.   }
  9. }
复制代码

六、最佳实践指南


  • 定名规范

    • 使用小写字母 + 连字符定名具名插槽(如 #user-avatar)
    • 避免使用保存字作为插槽名(如 default、item)

  • 性能优化
    1. <!-- 通过 v-if 控制插槽内容渲染 -->
    2. <template #header v-if="showHeader">
    3.   <HeavyComponent />
    4. </template>
    复制代码
  • 类型安全(TypeScript)
    1. // 定义作用域插槽类型
    2. defineSlots<{
    3.   default?: (props: { item: T; index: number }) => any
    4.   header?: () => any
    5.   footer?: () => any
    6. }>()
    复制代码

七、常见题目解答

Q1:如何强制要求必须提供某个插槽
  1. // 子组件中验证
  2. export default {
  3.   mounted() {
  4.     if (!this.$slots.header) {
  5.       console.error('必须提供 header 插槽内容')
  6.     }
  7.   }
  8. }
复制代码
Q2:插槽内容如何访问子组件方法?
  1. <!-- 子组件暴露方法 -->
  2. <slot :doSomething="handleAction"></slot>
  3. <!-- 父组件使用 -->
  4. <template #default="{ doSomething }">
  5.   <button @click="doSomething">触发子组件方法</button>
  6. </template>
复制代码
Q3:如何实现插槽内容过渡动画?
  1. <transition name="fade" mode="out-in">
  2.   <slot></slot>
  3. </transition>
复制代码

八、综合应用案例

可设置的模态框组件
  1. <!-- Modal.vue -->
  2. <template>
  3.   <div class="modal" v-show="visible">
  4.     <div class="modal-header">
  5.       <slot name="header">
  6.         <h2>{{ title }}</h2>
  7.         <button @click="close">×</button>
  8.       </slot>
  9.     </div>
  10.    
  11.     <div class="modal-body">
  12.       <slot :close="close"></slot>
  13.     </div>
  14.     <div class="modal-footer">
  15.       <slot name="footer">
  16.         <button @click="close">关闭</button>
  17.       </slot>
  18.     </div>
  19.   </div>
  20. </template>
复制代码
父组件使用
  1. <Modal v-model:visible="showModal" title="自定义标题">
  2.   <template #header>
  3.     <h1 style="color: red;">紧急通知</h1>
  4.   </template>
  5.   <template #default="{ close }">
  6.     <p>确认删除此项?</p>
  7.     <button @click="confirmDelete; close()">确认</button>
  8.   </template>
  9.   <template #footer>
  10.     <button @click="showModal = false">取消</button>
  11.   </template>
  12. </Modal>
复制代码



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

圆咕噜咕噜

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表