EventBus
EventBus 是极简的内存型事件总线。每个事件类型可挂多个监听器;'*'(WILDCARD)订阅所有事件;on 返回取消订阅函数;emit 按订阅顺序同步调用匹配的监听器。
概述
设计要点:
- 极简内存型,每个类型可挂多个监听器
'*'(WILDCARD) 订阅所有事件类型on返回一个取消订阅函数emit按订阅顺序同步调用匹配的监听器- fail-fast:不会捕获 handler 抛出的错误,监听器抛错会中断后续派发链
- 需要容错的引擎级 handler 请自行 try/catch 包裹逻辑
构造函数
ts
new EventBus()无参数构造。内部维护 listeners: Map<string, Set<EventListener>> 与 allListeners: Set<EventListener>(通配监听器集合)。
属性
| 属性 | 类型 | 说明 |
|---|---|---|
WILDCARD | '*' (常量) | 通配事件类型,订阅所有事件时使用 |
WILDCARD 是从模块导出的常量:
ts
export const WILDCARD = '*'方法
on()
订阅指定类型的事件。
ts
on(type: string, listener: EventListener): () => void参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
type | string | 是 | 事件类型;传 '*'(WILDCARD)订阅所有事件 |
listener | EventListener | 是 | 监听器函数 |
返回值
() => void —— 取消订阅函数,调用后移除该监听器。
行为
type === WILDCARD时把 listener 加入allListeners,返回的取消函数会从allListeners中移除- 否则按 type 取出 / 创建
Set<EventListener>,加入 listener,返回的取消函数调用off(type, listener)
ts
on(type: string, listener: EventListener): () => void {
if (type === WILDCARD) {
this.allListeners.add(listener)
return () => this.allListeners.delete(listener)
}
let set = this.listeners.get(type)
if (!set) {
set = new Set()
this.listeners.set(type, set)
}
set.add(listener)
return () => this.off(type, listener)
}示例
ts
import { EventBus } from 'decklet'
const bus = new EventBus()
const off = bus.on('CARD_PLAYED', (event) => {
console.log('card played:', event.payload)
})
// 取消订阅
off()once()
订阅一次:监听器首次被触发后自动取消订阅。
ts
once(type: string, listener: EventListener): () => void返回值
() => void —— 取消订阅函数(可在触发前手动移除)。
行为
内部包装 listener,在首次触发时调用 off(type, wrapper) 后再调用原 listener。
示例
ts
const off = bus.once('GAME_STARTED', (event) => {
console.log('game started (only once):', event.payload)
})
// 也可在触发前手动取消
// off()off()
移除指定类型下的某个监听器。
ts
off(type: string, listener: EventListener): void参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
type | string | 是 | 事件类型;'*' 时从通配集合中移除 |
listener | EventListener | 是 | 待移除的监听器 |
行为
type === WILDCARD时从allListeners中移除- 否则从对应 type 的 Set 中移除;如果 Set 变空则从 Map 中删除该 type
offAll()
清空所有监听器(含通配监听器)。
ts
offAll(): voidGameEngine.destroy() 会调用此方法。
emit()
同步派发事件:按注册顺序依次调用匹配的监听器。
ts
emit(event: GameEvent): void参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
event | GameEvent | 是 | 待派发的事件 |
行为
- 派发顺序:先通配监听器(按注册顺序),再具名类型监听器
- 监听器抛出的异常会中断后续派发(fail-fast),需要容错的监听器请自行 try/catch
ts
emit(event: GameEvent): void {
// 先调用通配监听器(按注册顺序),再调用具名类型监听器
if (this.allListeners.size > 0) {
for (const l of this.allListeners) {
l(event)
}
}
const set = this.listeners.get(event.type)
if (set) {
for (const l of set) {
l(event)
}
}
}注意
- 通配监听器先于具名类型监听器被调用
- 监听器抛错会中断后续派发,但不会改变 state(emit 仅派发已构造完成的事件)
GameEngine在processAction内调用emit时,事件已经通过eventFromInit补全元数据
listenerCount()
返回监听器数量。
ts
listenerCount(type?: string): number参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
type | string | 否 | 事件类型;传 '*' 返回通配监听器数量;省略则返回全部监听器总数 |
返回值
number —— 监听器数量。
行为
type === undefined→ 返回所有监听器(通配 + 各类型)总数type === WILDCARD→ 返回通配监听器数量- 其他 → 返回指定 type 下的监听器数量
hasListeners()
是否存在任意监听器。
ts
hasListeners(): booleanEventListener 类型
事件监听器函数:接收一个 GameEvent。
ts
export type EventListener = (event: GameEvent) => voidEventHandler 接口
事件处理器:按 type 订阅事件,被触发时调用 handle。type 为 '*'(WILDCARD)时订阅所有事件。
ts
export interface EventHandler {
type: string
handle(event: GameEvent): void
}| 字段/方法 | 类型 | 说明 |
|---|---|---|
type | string | 订阅的事件类型,'*' 表示订阅所有 |
handle | (event: GameEvent) => void | 处理函数 |
EventHandler 是 GamePlugin.events 元素的接口。GameEngine.wirePlugins 把插件的 events 注册到 EventBus:
ts
for (const e of p.events ?? []) {
this.eventBus.on(e.type, (event) => {
if (this.destroyed) return
e.handle(event, this.context)
})
}要点:引擎统一包裹在 destroyed 检查中,防止引擎销毁后仍触发 Plugin 逻辑。
示例
基本使用
ts
import { EventBus, WILDCARD } from 'decklet'
const bus = new EventBus()
// 订阅特定类型
const off1 = bus.on('CARD_PLAYED', (event) => {
console.log('card played:', event.payload)
})
// 订阅所有事件
const off2 = bus.on(WILDCARD, (event) => {
console.log('[any]', event.type, event.payload)
})
// 一次性订阅
bus.once('GAME_STARTED', (event) => {
console.log('game started (only once)')
})
// 查询
console.log(bus.listenerCount()) // 全部监听器总数
console.log(bus.listenerCount('CARD_PLAYED')) // 指定 type 数量
console.log(bus.listenerCount(WILDCARD)) // 通配监听器数量
console.log(bus.hasListeners()) // true
// 取消订阅
off1()
off2()
// 清空所有
bus.offAll()通过 GameEngine 订阅
ts
import { GameEngine } from 'decklet'
const engine = new GameEngine()
// 订阅指定类型
const off = engine.subscribe('CARD_PLAYED', (event) => {
console.log('card played:', event.payload)
})
// 等价写法
const off2 = engine.subscribeAll((event) => {
console.log(`[event] ${event.type}`, event.payload)
})
// 取消订阅
off()
off2()在 Plugin 中处理事件
ts
import { type GamePlugin } from 'decklet'
export const MyPlugin: GamePlugin = {
id: 'my-plugin',
name: 'My Plugin',
version: '1.0.0',
events: [
{
type: 'CARD_PLAYED',
handle(event, ctx) {
ctx.setVariable('lastPlayedAt', event.timestamp)
// 链式 dispatch
// ctx.dispatch(createEndTurnAction(event.payload.playerId))
}
},
{
type: '*', // 订阅所有事件
handle(event, ctx) {
// log event
}
}
]
}注意事项
- fail-fast:监听器抛错会中断后续派发,需要容错的监听器请自行 try/catch
- 通配监听器先于具名类型监听器被调用
on返回的取消订阅函数应在不需要时调用,避免内存泄漏GameEngine.destroy()会调用offAll(),销毁后所有监听器都会被移除EventHandler.handle在引擎销毁后不会被调用(wirePlugins内有destroyed检查)- 通过
GameEngine.subscribe订阅的监听器与插件提供的 EventHandler 共享同一个EventBus