本示例实现了一个简单的 事件总线(EventBus),可让不同组件在不直接互相引用的情况下实现事件的订阅与发布。同时,还提供了 useEmit
与 useOn
两个自定义 Hook,用来更灵活地发送/监听事件。
功能概览
- 发布 - 订阅:在一个全局
Emitter
实例上注册事件,通过emit(eventName, ...args)
发布事件,通过on(eventName, callback)
订阅事件。 - 事件记忆:如果在订阅之前已经触发过某个事件,新订阅者仍能立刻收到最新一次触发的数据(通过
_prevEvents
进行记录)。 - Hook 封装:
useEmit
:返回一个emit
函数,可在函数组件里直接调用进行事件发布。useOn
:在函数组件中订阅事件,并在组件卸载时自动取消订阅,避免内存泄漏。
核心代码
Emitter 类
class Emitter implements EventBus {
private _events = new Map<string, EventFn[]>();
private _prevEvents = new Map<string, EventParams[]>();
emit(event: string, ...args: EventParams) {
const fns = this._events.get(event);
const allFns = this._events.get('*'); // 预留通配功能
// 如果有监听 '*'
if (allFns) {
fns?.forEach(fn => fn(...args));
}
if (fns) {
fns.forEach(fn => fn(...args));
} else {
// 如果还没有人订阅这个事件,则暂时将参数保存起来
if (!this._prevEvents.has(event)) {
this._prevEvents.set(event, []);
}
this._prevEvents.get(event)?.push(args);
}
}
on(event: string, fn: EventFn) {
const fns = this._events.get(event);
if (fns) {
fns.push(fn);
} else {
this._events.set(event, [fn]);
}
// 如果在订阅之前已经触发过该事件,则立刻把缓存的参数传给新订阅者
if (this._prevEvents.has(event)) {
this._prevEvents.get(event)?.forEach(args => fn(...args));
this._prevEvents.delete(event);
}
}
off(event: string, fn: EventFn) {
const fns = this._events.get(event);
if (fns) {
if (fns.length === 1) {
this._events.delete(event);
} else {
this._events.set(
event,
fns.filter(f => f !== fn)
);
}
}
}
offAll() {
this._events.clear();
this._prevEvents.clear();
}
}
关键点
字段/方法 | 说明 |
---|---|
_events | 存储所有已订阅的事件及对应回调函数的数组,格式类似 Map<string, EventFn[]> 。 |
_prevEvents | 保存“事件已触发但尚无订阅者”时对应的参数列表。新订阅者可以在订阅后立即获取之前触发的该事件参数。 |
emit(event, ...args) | 发布事件。如果当前 event 有已注册的回调,则依次调用这些回调;如果没有任何订阅者,则将参数暂存到 _prevEvents ,以便未来订阅者能立刻获取历史触发记录。 |
off(event, fn) | 移除特定事件 event 上的指定回调 fn 。若该事件只有一个回调,移除后直接从 _events 中删除;否则仅在原数组中过滤掉此回调。 |
offAll() | 清空所有事件及回调函数,同时清空 _prevEvents 。 |
全局实例
export const emitter = new Emitter();
- 全项目共享的事件总线,可在任意位置导入并使用。
自定义 Hook
export function useEmit() {
return (event: string, ...args: EventParams) => emitter.emit(event, ...args);
}
export function useOn(event: string, fn: EventFn) {
const stableFn = useCallback(fn, [fn]);
useEffect(() => {
emitter.on(event, stableFn);
return () => emitter.off(event, stableFn);
}, [event, stableFn]);
}
useEmit()
:返回一个函数,与emitter.emit
等效。可在组件或其他 Hook 中直接调用以触发事件。useOn()
:在函数组件中订阅指定事件,组件卸载时自动off
,防止内存泄漏。- 使用
useCallback
保证回调引用一致,否则useEffect
的依赖可能在每次渲染都导致重新订阅。
- 使用
使用方式
发布事件
在组件或 Hook 中使用:
import React from 'react';
import { useEmit } from '@/hooks/eventBus';
function Publisher() {
const emit = useEmit();
const handleClick = () => {
emit('testEvent', 'Hello World!');
};
return <button onClick={handleClick}>发送事件</button>;
}
- 点击后会向全局事件总线发送
'testEvent'
,并携带'Hello World!'
作为参数。
订阅事件
在另一个组件或 Hook 中监听事件:
import React from 'react';
import { useOn } from '@/hooks/eventBus';
function Subscriber() {
useOn('testEvent', message => {
console.log('收到 testEvent:', message);
});
return <div>我是订阅者</div>;
}
- 当任意地方调用
emit('testEvent', someValue)
,Subscriber
就能收到并执行回调。
未订阅时提前触发
如在某些场景下,你在发布事件时尚无任何组件订阅该事件,这些触发数据会暂存在 _prevEvents
。当后续组件使用 on('someEvent')
订阅时,就会立刻收到之前触发的数据,然后 _prevEvents
对应的事件缓存被清除。
适用场景
- 组件间通信:父子、非父子组件间传递信息,而不必过度依赖 Redux / Context 等全局状态管理。
- 解耦逻辑:可让某些通用信息在全局任意位置产生与消费,无需直接建立依赖关系。
- SSR / 非持久场景:事件总线适用于短周期的临时通信,若需要持久化或历史记录,则考虑其他方案(如 Redux store / local storage 等)。
总结
通过 Emitter
类与全局实例,你能在不增加依赖复杂度的前提下完成跨组件的事件通信:
emitter.on/off/emit
提供最基础的发布-订阅模式。useEmit
与useOn
将事件操作简化为 React 函数式组件可用的形式,在合适的时机(挂载/卸载)自动订阅和取消订阅。- 事件缓存
_prevEvents
**:当事件在无订阅者时触发,也能缓存最后一次参数,后续新增订阅者可立刻拿到数据。
这样的事件总线在多组件通信、解耦业务逻辑方面能起到很好的效果,是一个轻量级且灵活的解决方案。
Last updated on