广播
介绍
向所有自身、子组件和后代组件广播的 Vue 组合式 API Hook。基于 provide/inject
实现。
假设又如下组件结构:
Parent.vue
├── Child1.vue
└── Child2.vue
├── Child2_1.vue
└── Child2_2.vue
我们在 Parent.vue
中使用 useBroadcast()
创建一个广播器实例,那么 Parent.vue
的子组件和后代组件就组成了一个广播域。在这个域中任意组件发出广播,整个域中的组件都会收到这个广播。
利用这一套组合式 API,就可以在一个域中实现子传父、父传子、子子传父,父传子子的传递数据。有点类似于 Vue 2 的事件总线,但是组合式 API 更加灵活,而且使用 provide/inject
范围不会那么广,更适合在小范围中使用。
注意
一个域中不能有两个及以上的广播器实例,不然可能会冲突。
示例
父组件
子组件
父组件或祖先组件:
vue
<script setup lang="ts">
import { useBroadcast } from './broadcast'
import ChildComponent from './example-child.vue'
const { broadcast, receive } = useBroadcast()
// 广播事件
const sendMessage = () => {
broadcast('message', { text: 'Hello from parent!' })
}
const sendNotification = () => {
broadcast('notification', 'This is a notification')
}
receive('message', (data) => {
console.log('父组件收到消息:', data)
})
receive('notification', (data) => {
console.log('父组件收到通知:', data)
})
</script>
<template>
<div class="parent-component">
父组件
<button class="native" @click="sendMessage">发送消息</button>
<button class="native" @click="sendNotification">发送通知</button>
<ChildComponent />
</div>
</template>
<style scoped>
.parent-component {
padding: 20px;
border: 1px solid var(--vp-c-border);
border-radius: 6px;
}
</style>
子组件:
vue
<script setup lang="ts">
import { useChildBroadcast, useReceiveBroadcast } from './broadcast'
// 接收消息广播
useReceiveBroadcast<{ text: string }>('message', (data) => {
console.log('子组件收到消息:', data.text)
})
// 接收通知广播
useReceiveBroadcast('notification', (data) => {
console.log('子组件收到通知:', data)
})
// 忽略来自当前组件实例的广播
useReceiveBroadcast('notification', (data) => {
console.log('子组件收到通知(忽略来自当前组件实例的广播):', data)
}, { excludeSelf: true })
// 只接收一次
useReceiveBroadcast('notification', (data) => {
console.log('子组件收到通知(只接收一次):', data)
}, { once: true })
const broadcast = useChildBroadcast()
// 广播事件
const sendMessage = () => {
broadcast('message', { text: 'Hello from parent!' })
}
const sendNotification = () => {
broadcast('notification', 'This is a notification')
}
</script>
<template>
<div class="child-component">
子组件
<button class="native" @click="sendMessage">发送消息</button>
<button class="native" @click="sendNotification">发送通知</button>
</div>
</template>
<style scoped>
.child-component {
padding: 20px;
border: 1px solid var(--vp-c-border);
border-radius: 6px;
margin-top: 20px;
}
</style>
代码
ts
// broadcast.ts
import { provide, inject, onUnmounted, getCurrentInstance } from 'vue'
import type { InjectionKey } from 'vue'
/**
* 广播处理函数
*/
export type BroadcastHandler<T = unknown> = (data: T) => void
/**
* 广播函数
*/
export type Broadcast = (type: string, data?: unknown) => void
/**
* 接收广播函数
*/
export type BroadcastReceive = <T = unknown>(
type: string,
handler: BroadcastHandler<T>,
options?: BroadcastReceiveOptions,
) => () => void
/**
* 接收广播函数配置项
*/
export interface BroadcastReceiveOptions {
/**
* 是否只接收一次
* @default false
*/
once?: boolean
/**
* 是否排除自身
* @default false
*/
excludeSelf?: boolean
}
/**
* 监听器组成
*/
interface ListenerMaterial {
handler: BroadcastHandler<any>
once?: boolean
excludeSelf?: boolean
uid?: number | undefined
}
/**
* 广播器
*/
class Broadcaster {
private readonly listeners = new Map<string, ListenerMaterial[]>()
on(
type: string,
handler: BroadcastHandler<any>,
once: boolean,
excludeSelf: boolean,
uid: number | undefined,
) {
let materials = this.listeners.get(type)
let repeated = false
if (materials) {
repeated = materials.some((item) => item.handler === handler)
} else {
materials = []
this.listeners.set(type, materials)
}
// 忽略重复的监听器
if (!repeated) {
materials.push({ handler, once, excludeSelf, uid })
}
// 返回取消监听的函数
return () => this.off(type, handler)
}
off(type: string, handler: BroadcastHandler<any>) {
const materials = this.listeners.get(type)
if (!materials) return
const index = materials.findIndex((item) => item.handler === handler)
if (index !== -1) {
materials.splice(index, 1)
}
if (materials.length === 0) {
this.listeners.delete(type)
}
}
emit(type: string, data: unknown, uid?: number) {
const materials = this.listeners.get(type)
if (!materials) return
// 复制一份 materials 避免在迭代过程中被修改
Array.from(materials).forEach((material) => {
try {
if (
material.excludeSelf &&
uid !== void 0 &&
material.uid !== void 0 &&
material.uid === uid
) {
// 忽略来自当前组件实例的广播
return
}
material.handler(data)
} catch (error) {
console.error(`Error handling broadcast event "${type}":`, error)
}
if (material.once) {
this.off(type, material.handler)
}
})
}
}
// 注入键
const BROADCAST_KEY: InjectionKey<Broadcaster> = Symbol('broadcast')
/**
* 广播 Hook
* 用于向所有子组件和后代组件广播事件
* @returns 广播函数和接收广播函数
*
* @example
* const { broadcast, receive } = useBroadcast();
*/
export const useBroadcast = (): {
broadcast: Broadcast
receive: BroadcastReceive
} => {
const uid = getCurrentInstance()?.uid
// 创建广播器实例
const broadcaster = new Broadcaster()
// 提供广播器给子组件
provide(BROADCAST_KEY, broadcaster)
const offs: Array<() => void> = []
onUnmounted(() => {
// 组件卸载时移除所有监听器
offs.forEach((fn) => fn())
})
const broadcast: Broadcast = (type, data) => {
return broadcaster.emit(type, data, uid)
}
const receive: BroadcastReceive = (type, handler, options) => {
const { once = false, excludeSelf = false } = options || {}
if (excludeSelf && uid === void 0) {
console.warn('Failed to obtain uid, excludeSelf option is invalid')
}
offs.push(() => broadcaster.off(type, handler))
return broadcaster.on(type, handler, once, excludeSelf, uid)
}
return { broadcast, receive }
}
/**
* 接收广播 Hook
* 用于监听父级或祖先组件广播的事件
* @param type 要监听的事件类型
* @param handler 事件处理函数
*
* @example
* const broadcast = useReceiveBroadcast('event', (data) => {
* console.log(data)
* })
*/
export const useReceiveBroadcast: BroadcastReceive = (
type,
handler,
options,
) => {
const { once = false, excludeSelf = false } = options || {}
const uid = getCurrentInstance()?.uid
if (excludeSelf && uid === void 0) {
console.warn('Failed to obtain uid, excludeSelf option is invalid')
}
// 获取父级广播器
const broadcaster = inject(BROADCAST_KEY, null)
if (!broadcaster) {
console.warn(`No broadcast provider found for event type "${type}"`)
return () => {}
}
// 组件卸载时自动移除监听器
onUnmounted(() => {
broadcaster.off(type, handler)
})
// 注册监听器
return broadcaster.on(type, handler, once, excludeSelf, uid)
}
/**
* 子组件广播 Hook
* 用于在子组件中发送广播事件给父组件或兄弟组件
* @returns 广播函数,可用于发送事件
*
* @example
* const broadcast = useChildBroadcast();
* broadcast('event', { message: 'hello' });
*/
export const useChildBroadcast = (): Broadcast => {
const uid = getCurrentInstance()?.uid
if (uid === void 0) {
console.warn('Failed to obtain uid, excludeSelf option is invalid')
}
const broadcast = inject(BROADCAST_KEY)
if (!broadcast) {
console.warn(`No broadcast provider found`)
}
const childBroadcast = (type: string, data?: unknown) => {
if (!broadcast) return
broadcast.emit(type, data, uid)
}
return childBroadcast
}