React 中优雅的使用 useEventEmitter 进行多组件通信

4,060 阅读2分钟

🌍 背景

在前端项目业务中,组件间的通讯是十分频繁的。父传子,子传父,兄弟间等等。在多个组件之间进行事件通知有时会让人非常头疼,借助 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);
  });