Skip to Content
🎉 Nextra 4.0 is released. dimaMachina is looking for a new job or consulting.
hooks自定义事件总线 (EventBus) 与 Hook

本示例实现了一个简单的 事件总线(EventBus),可让不同组件在不直接互相引用的情况下实现事件的订阅与发布。同时,还提供了 useEmituseOn 两个自定义 Hook,用来更灵活地发送/监听事件。

功能概览

  1. 发布 - 订阅:在一个全局 Emitter 实例上注册事件,通过 emit(eventName, ...args) 发布事件,通过 on(eventName, callback) 订阅事件。
  2. 事件记忆:如果在订阅之前已经触发过某个事件,新订阅者仍能立刻收到最新一次触发的数据(通过 _prevEvents 进行记录)。
  3. 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 类与全局实例,你能在不增加依赖复杂度的前提下完成跨组件的事件通信:

  1. emitter.on/off/emit 提供最基础的发布-订阅模式。
  2. useEmituseOn 将事件操作简化为 React 函数式组件可用的形式,在合适的时机(挂载/卸载)自动订阅和取消订阅。
  3. 事件缓存 _prevEvents**:当事件在无订阅者时触发,也能缓存最后一次参数,后续新增订阅者可立刻拿到数据。

这样的事件总线在多组件通信、解耦业务逻辑方面能起到很好的效果,是一个轻量级且灵活的解决方案。

Last updated on