我这个消息号是为了兼容其它非rpc网络协议
分享下目前在用的事件系统,用了几个项目也没遇到什么问题,胜在简单粗暴。
事件系统:
export class EventCenter {
private static eventMap: Map<string, Array<{callback: Function, target?: any}>> = new Map();
// 注册事件,增加target参数
public static registerEvent(eventName: string, callback: Function, target?: any): void {
if (!this.eventMap.has(eventName)) {
this.eventMap.set(eventName, []);
}
this.eventMap.get(eventName)?.push({ callback, target });
}
// 触发事件
public static triggerEvent(eventName: string, ...args: any[]): void {
const handlers = this.eventMap.get(eventName);
if (handlers) {
handlers.forEach(({ callback, target }) => {
if (target) {
callback.apply(target, args);
} else {
callback(...args);
}
});
}
}
// 移除事件
public static removeEvent(eventName: string, callback: Function, target?: any): void {
const handlers = this.eventMap.get(eventName);
if (handlers) {
const index = handlers.findIndex(item =>
item.callback === callback && item.target === target
);
if (index !== -1) {
handlers.splice(index, 1);
}
}
}
}
注册与移除:
protected onEnable(): void {
//参数1:自定义事件名 参数2:要绑定的方法 参数3:绑定当前类
EventCenter.registerEvent('UpdateHP', this.updateBarValue, this);
}
protected onDisable(): void {
EventCenter.removeEvent('UpdateHP', this.updateBarValue, this);
}
触发:
//参数1:自定义事件名 后续参数:任意数量和类型的参数,对应注册方法
EventCenter.triggerEvent("UpdateHP", this.hp , isAnim);
emit<T extends keyof CT, T2 extends Parameters<CT[T]>>
这个我不太了解,其他语言一般都没有这种写法。问了一下AI。
TypeScript 语法解析
emit<T extends keyof CT, T2 extends Parameters<CT[T]>>(type_: T | T[], ...args_: T2): void
这行代码定义了一个泛型方法 emit 。我们来分解它的泛型参数:
-
CT:
- 这是
SafeEventTarget<CT>类定义中的泛型参数。它代表一个“契约类型 (Contract Type)”或者说是一个事件定义的集合。 - 在你的例子中
interface Event { test(a: number): void; },当创建new SafeEventTarget<Event>()时,CT就被实例化为Event这个接口类型。
-
T extends keyof CT:
- 这是一个泛型参数
T。 -
keyof CT: 这是一个 TypeScript 的操作符,它会获取类型CT的所有公共属性名(键)组成的联合类型。- 对于
interface Event { test(a: number): void; },keyof Event的结果是"test"(如果 Event 有更多属性,比如close(): void;,那么keyof Event就是"test" | "close")。
- 对于
-
T extends keyof CT: 这叫做泛型约束。它意味着T必须是CT的一个键。换句话说,T只能是CT中定义的事件名之一。- 在你的例子中,
T只能是"test"。
- 在你的例子中,
- 这个约束确保了
emit函数的第一个参数type_(当它不是数组时) 必须是一个在CT中真实存在的事件名。如果你尝试emit("nonExistentEvent", ...),TypeScript 会在编译时报错。
-
T2 extends Parameters<CT[T]>:
- 这是第二个泛型参数
T2。 -
CT[T]: 这叫做索引访问类型 (Indexed Access Type) 或查找类型 (Lookup Type)。它会获取类型CT中属性T的类型。- 如果
CT是Event,T是"test",那么CT[T](即Event["test"]) 的类型就是(a: number) => void(一个函数类型)。
- 如果
-
Parameters<FuncType>: 这是一个 TypeScript 内置的工具类型 (Utility Type)。它会提取函数类型FuncType的参数类型,并将它们构造成一个元组 (tuple) 类型。- 对于
(a: number) => void,Parameters<(a: number) => void>的结果是[number](一个只包含一个number类型元素的元组)。
- 对于
-
T2 extends Parameters<CT[T]>: 这又是一个泛型约束。它意味着T2必须是(或兼容于)由CT[T](即事件对应的函数类型) 的参数所组成的元组类型。 - 这个约束确保了
emit函数的剩余参数...args_(这是一个 rest parameter,会收集所有后续参数到一个数组/元组中) 必须与CT中为事件T定义的函数的参数类型和数量相匹配。- 在你的例子中,因为
Parameters<Event["test"]>是[number],所以...args_必须是一个number类型的参数。emit("test", 0)是合法的,但emit("test", "hello")或emit("test", 0, 1)就会在编译时报错。
- 在你的例子中,因为
总结 TypeScript 的优势:
- 类型安全 :编译器会检查事件名是否存在,以及传递的参数是否与事件定义的参数类型匹配。
-
智能提示 (IntelliSense) :IDE 可以根据
CT的定义,在你输入event.emit(时提示合法的事件名,并在选中事件名后提示所需的参数类型。 -
重构友好 :如果修改了
Event接口中某个事件的签名(例如参数类型或数量),TypeScript 会在所有不匹配的emit调用处报错。
TypeScript 的类型系统在这方面确实展现了其独特之处和强大能力。让我们再深入一点理解为什么:
-
结构化类型系统 (Structural Typing) : TypeScript 的类型系统主要是结构化的(“duck typing”)。如果一个对象具有某个接口要求的所有属性和方法(且类型兼容),它就被认为是实现了该接口,无需显式声明
implements。这使得keyof和索引访问类型CT[T]的概念更加自然和强大。它关心的是“形状”而不是“名称”或“声明”。 -
字面量类型 (Literal Types) : TypeScript 支持字符串字面量类型 (e.g.,
type EventName = "click" | "hover").keyof CT正是产生了一个由字符串字面量组成的联合类型。这是能够将“键的名称”提升到类型层面进行操作的关键。 -
映射类型 (Mapped Types) 和条件类型 (Conditional Types) : 虽然这个例子中没有直接大量使用映射类型(
key属性的类型{[k in keyof CT]: k}是一个简单的映射类型),但它们是 TypeScript 类型系统能进行复杂类型转换和推断的基石。Parameters<T>和ReturnType<T>这样的工具类型内部就是用条件类型和infer关键字等高级特性实现的。
为什么其他静态类型语言的泛型通常做不到这一点?
- 名义类型系统 (Nominal Typing) : 大多数主流静态类型语言(如 C++, C#, Java)主要采用名义类型系统。类型是通过其声明的名称来区分的。这意味着即使两个类有完全相同的结构,如果它们名称不同,它们就是不同的类型。这使得“获取任意类型的键并将其用作类型”这种操作不那么直接。
-
泛型主要关注“类型参数化”,而非“结构内省” :
- 在 C# 或 Java 中,泛型
List<T>意味着这个列表可以存储任何类型T的对象。T是一个占位符,代表一个完整的类型。 - 它们通常没有内置的、在泛型约束中直接使用的机制来:
- 获取一个泛型参数
CT的所有成员名称,并将其变成一个可在编译时使用的“键的类型集合”(类似 TypeScript 的keyof CT产生的字符串字面量联合类型)。 - 使用这个“键的类型”(如果是字符串)来静态地、类型安全地索引
CT以获取成员的类型(类似CT[T])。
- 获取一个泛型参数
- 在 C# 或 Java 中,泛型
-
反射 vs. 静态类型操作 :
- C# 和 Java 等语言可以通过 反射 (Reflection) 在运行时获取类的成员信息(名称、类型等)。但这是运行时的行为,通常会丢失编译时的类型安全,或者需要编写大量防御性代码。
- TypeScript 的
keyof和CT[T]是 编译时 的静态类型操作,编译器在编译阶段就能进行检查和推断。
-
C++ 模板元编程 : C++ 的模板元编程非常强大,理论上可以在编译时做很多事情。但要实现与 TypeScript 完全等价的、基于字符串字面量(或类似物)的
keyof和索引访问会非常复杂,通常需要复杂的模板技巧、宏,或者依赖于未来的反射特性(如 C++23 或之后提案的静态反射)。它通常不以这种“基于名称字符串的类型查找”为主要设计范式。C++ 更倾向于使用类型本身、枚举或特化来区分和操作。
总结来说:
TypeScript 的设计目标之一就是为 JavaScript 提供强大的静态类型检查,同时尽可能地贴合 JavaScript 的动态特性。因此,它的类型系统发展出了很多独特的功能,允许在类型层面进行非常灵活和精细的操作,比如根据一个类型的“形状”来推断和约束其他类型。
其他静态语言的泛型系统虽然也非常强大,但它们的设计哲学和演进路径不同,通常不直接提供这种“以成员名称(作为类型)来驱动泛型类型推断”的机制。它们可以通过其他方式(如接口、继承、特定的设计模式、或 C# 的 Source Generators,C++ 的模板特化等)来实现类型安全的事件系统,但实现 emit<T extends keyof CT, T2 extends Parameters<CT[T]>> 这种签名所依赖的 编译时静态推断链条 会困难得多,或者说不是其泛型系统的原生设计目标。
看起来甚至连C++的模版元编程都做不到这一点。
基于这个机制,就可以大量简化类型安全所需要的设施了。

MK框架也有消息号,但是在用户层可以隐藏,重要的是实现这两种方式我都没改框架层后续还可以扩展更多
并且支持两种消息号的方式
第一种
message Package {
/** 消息号 */
int32 id = 1;
/** 消息体 */
bytes data = 2;
}
第二种
message test {
int32 id [default = 100];
}
底层看你自己选择了,萝卜青菜各有所爱,我是基于 proto 开发的
好好好,cv一下
不明白你这两种方式有啥区别,我是为了更贴合真实RPC调用才这样实现的,比如远程服务器有一个RPC服务:
前端TS调用可以这样:
Global.remotingInvoke("DemoService:sum", [1, 1000]).then(res => {
console.log("远程计算两数之间所有数字之和:" + res);
}).catch(err => {
console.log(err);
});
还可以如下这样:
服务端:
客户端调用:
我是从 tsrpc 这个项目中第一次看到这种用法,这种无论怎样,都是要有一个数据和消息的关连关系来辅助类型推断。
要么在消息数据体 data 中,要么是对应关系映射表。
其中映射表的方式除了集中式(事件一般是集中式)。还可以用 global declare {
interface XX {
[event_name]: event_data_type
}
}
进行分布式定义。而这种机制可以泛化到【类型到类】【类型到函数】【类型到类型】,这种机制的复用让理解成本更低。
TS的类型比C#要强大,很多C#办不到或很难办到的,用TS就比较好实现。
按我的理解事件系统是为了解决“你中无我,我中无你,但是我们俩还想通信”,或者是为了达到“你中无我,我中无你”的状态而引入事件系统以求解耦。还有一种情况比较简单“你中有我,但是我中不能有你,因为可能循环引用”,虽然可以用接口去解决,但是也可以用事件系统达到目的。
题主的事件系统能搞这么复杂,也是让人头皮发麻,没有看下去的欲望,我一度以为我理解的事件系统和题主理解的不是一回事,我都懒得造轮子,感觉造出来也没啥意义,直接用的EventEmitter3,只是简单封装了下,封装的目的是为了方便使用仅此而已。
源码:
例子:
代码提示:
我觉得这就够了,而且type申明的类型构建后也擦除了
这个类型 最后其实就是直接调用了那个函数本身 不算是发消息就是找到那个对象的地址直接发消息差不多的样子,事件系统一般就是向世界抛出一个事件谁监听了这个时间 谁就可以获取到这个事件的数据
直接用game.emit 感觉一样的呀 
就是为了解耦才用事件,结果还给事件类型耦合起来…
楼主是为了解决数据类型约束,不过方案过于复杂
耦合的主体有区别,你说的是调用主体解耦。楼主说的是事件派发的数据类型约束
你这个 IEvent 类型方案其实和 MK 的类似,我以前尝试过,结论是:不好用。会有几个问题点
1、集中式的声明。有些逻辑其实是分包的,这个 IEvent 没有支持分布式定义。(当然 declare global 可解决这个问题)
2、所有事件都要声明(这个比较蛋疼,越用越难受)
3、事件处理函数有时候是多个函数。有的函数只心事件,不关心参数,通常是类似 refreshUI() 的刷新函数,而注册时就会报红,导致必须加 as any 这类的特殊处理。难受。
我的方案就是,通过约定做隐式数据约束。
emit<DATA = any>(name: string | number, data: DATA = null)
on<DATA = any>(name: string | number, cb: (data?: DATA) => any, ctx: any): EventBox;
于是,在开发的时候:
1、派发的地方有数据,前面的类型一定要补上,方便知道是什么数据类型;
2、监听的地方,一定根据派发的数据约束写上数据约束(不写表示只关心事件本身);
3、在新增消息处理函数的时候,只要查一下别的地方怎么用,然后做同样的约束。
4、派发数据更改时(重构)的时候。也是查下消息的引用。统一修改各个监听函数的处理逻辑的。
这套逻辑流程用起来最舒服。







