🌍 背景
在前端项目业务中,组件间的通讯是十分频繁的。父传子,子传父,兄弟间等等。在多个组件之间进行事件通知有时会让人非常头疼,借助 EventEmitter ,可以让这一过程变得更加简单。秉承着能白嫖绝不自己动手的原则,我翻看了ahooks,里面 useEventEmitter
可以进行多个组件通讯,具体原理主要是通过 props
或者 Context
共享一个全局的类实例,使用下来体验感不太好,它无事件名称,需要自己在传值处手动管理事件。。。 因此决定基于ahooks的 useEventEmitter
进行改进。
📖 期望
- 可以多组件通讯;
- 可以emit传递事件名,通过on接收,类似vue的eventBus;
- 可以全局共享,也能局部共享;
📡 全新的useEventEmitter
1.1、主要功能
主要功能分为两大类,局部共享和全局共享
- 通过global配置是否属于全局共享;
- 全局共享特点为,凡是使用改hook的组件内都具备全局共享的能力,不需要在最顶层传递event实例。属于同一个实例;
- 局部共享的特点为,凡是局部共享都需要传递event实例,可创建多个局部共享实例。属于同一个类;
- 全局和局部的事件相互独立;
1.2、 原理实现
- 声明一个类,类中定义一个私有的map
- emit相当于map的set操作,on相当于get操作取得传入的参数,通过listener回调
1.3、贴源码👇
event.js
import { useRef, useEffect } from 'react'
import { cloneDeep } from 'lodash'
type SubscriptionParams<T = any> = {
params: T
event: string | number
}
type Subscription<T> = ({ params, event }: SubscriptionParams<T>) => void
class EventEmitter<T> {
private subscriptions = new Map<string | number, Subscription<T>[]>()
private emitEffectCache = new Map<string | number, SubscriptionParams<T>>()
constructor() {
this.clear()
}
useSubscription = (event: string, listener?: Subscription<T>) => {
const callbackRef = useRef<Subscription<T>>()
useEffect(() => {
callbackRef.current = listener
function subscription(val: SubscriptionParams) {
if (callbackRef.current) {
callbackRef.current(val)
}
}
const subscriptions = this.subscriptions?.get(event) ?? []
subscriptions.push(subscription)
this.subscriptions.set(event, subscriptions)
// @ts-ignore
this.emitEffect(event)
return () => {
this.subscriptions.delete(event)
}
}, [])
}
emit = (event: string | number, ...args: T extends any[] ? any[] : any) => {
if (typeof event === 'string' || typeof event === 'number') {
const subscriptionValuesCallback = this.subscriptions.get(event)
subscriptionValuesCallback?.forEach((callback) => {
callback?.({
params: cloneDeep(args) as any,
event,
})
})
this.emitEffectCache.set(event, {
params: cloneDeep(args) as any,
event,
})
} else throw new TypeError('event must be string or number !')
}
emitEffect = (event: string | number) => {
const emitEffectCache = this.emitEffectCache.get(event)
const listeners = this.subscriptions.get(event)
if (emitEffectCache)
listeners?.forEach((listener) => {
listener?.({
...emitEffectCache,
})
})
}
removeListener = (event: string) => {
this.subscriptions.delete(event)
}
clear = () => {
this.subscriptions.clear()
}
}
const eventEmitterOverall = new EventEmitter();
export { EventEmitter, eventEmitterOverall };
index.ts
import { useCreation } from 'ahooks'
import { useEffect, useMemo, useRef } from 'react'
import { EventEmitter, eventEmitterOverall } from './event'
export default function useChartEventEmitter<T = void>(options?: {
global?: boolean
}) {
const ref = useRef<EventEmitter<T> | typeof eventEmitterOverall>()
const eventEmitterOptions = useMemo(
() => options ?? { global: false },
[options]
)
const event = useCreation(() => {
if (!ref.current) {
ref.current = eventEmitterOptions.global
? (ref.current = eventEmitterOverall)
: (ref.current = new EventEmitter())
}
return ref.current
}, [eventEmitterOptions])
useEffect(() => {
return () => event?.clear()
}, [event])
return event
}
🔨 使用
const eventBus = useEventEmitter({ global: true });
eventBus?.emit("hello", { name: "react" }, { name: "typescript" });
eventBus?.useSubscription("hello", (value) => {
console.log("hello", value);
});