bit-event:一个零依赖的全局事件系统,模块解耦全靠它

它解决什么问题

模块之间互相调用,最开始图省事直接 import 对方然后调方法,写着写着就变成了一张互相纠缠的依赖网——UI 模块 import 了战斗模块,战斗模块又 import 了 UI 模块,改一个地方不知道会牵动多远。事件系统是解这个问题最常见的办法:谁也不认识谁,只通过事件名义广播,听的人自己决定接不接。

bit-event 是 bit-framework 里最基础的一个模块,零依赖,纯 TypeScript 实现,bit-ec 就是拿它做组件间通信的。

安装

npm install @gongxh/bit-event

核心用法

全局事件

import { GlobalEvent } from '@gongxh/bit-event';

// 监听
const id = GlobalEvent.add('mail:new', (mailId: number) => {
    console.log('收到新邮件', mailId);
}, this);

// 发送
GlobalEvent.send('mail:new', 1001);

// 只听一次,触发后自动移除,不用手动清理
GlobalEvent.addOnce('game:firstLogin', onFirstLogin);

移除监听,四种方式挑一种

GlobalEvent.remove(id);                          // 按具体的监听 ID
GlobalEvent.removeByName('mail:new');             // 这个事件名的所有监听都移除
GlobalEvent.removeByTarget(this);                 // 这个 target 挂的所有监听都移除
GlobalEvent.removeByNameAndTarget('mail:new', this); // 精确匹配名字+target

实际项目里用得最多的是 removeByTarget——组件销毁的时候,一句话把它注册过的所有监听清空,不用一个个记 ID:

onDestroy(): void {
    GlobalEvent.removeByTarget(this);
}

需要隔离的场景,自己 new 一个

如果某个子系统的事件不想跟全局事件混在一起(比如一个可以整体重置的小游戏内小游戏),创建独立实例,API 跟全局事件完全一样:

import { EventManager } from '@gongxh/bit-event';

class MiniGameContext {
    event = new EventManager();
}

// 退出小游戏时一次清空,不影响全局事件系统
miniGameContext.event.clearAll();

最佳实践

事件名建议用常量集中管理,别在业务代码里到处写字符串字面量,拼错一个字母编译期是发现不了的,只有运行时才会发现监听没触发。

// events.ts 集中定义
export const GameEvents = {
    MailNew: 'mail:new',
    FirstLogin: 'game:firstLogin',
} as const;

另外要小心事件循环:事件 A 的回调里发了事件 B,事件 B 的回调里又发了事件 A,这种链路在代码量小的时候看不出来,模块一多很容易绕出一个死循环,排查起来要一路顺着监听关系摸,比较费时间。

避坑提醒

  • 忘记在对象销毁时移除监听,是这类事件系统最常见的内存泄漏来源——已经销毁的对象的回调函数还挂在事件表里,事件一发送就会尝试调用一个”死对象”上的方法。养成 onDestroyremoveByTarget(this) 的习惯基本能规避。
  • sendsendToTarget 的区别别搞混:send 是广播给所有监听这个名字的人,sendToTarget 只发给指定的那个 target,其他监听同名事件的人收不到。
  • 需要能整体清空、跟全局事件互不干扰的场景,直接用独立的 EventManager 实例,不要把这类临时性、局部性的事件也塞进全局事件里,退出的时候不好统一清理。

项目信息

bit-ec 用它做组件间通信,是目前唯一把它当 peer 依赖的模块,但它本身完全独立,任何 TypeScript 项目都能直接拿去用,不限定 Cocos Creator。