Skip to content

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

参数 ​

参数类型必填说明
typestring是事件类型;传 '*'(WILDCARD)订阅所有事件
listenerEventListener是监听器函数

返回值 ​

() => 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

参数 ​

参数类型必填说明
typestring是事件类型;'*' 时从通配集合中移除
listenerEventListener是待移除的监听器

行为 ​

  • type === WILDCARD 时从 allListeners 中移除
  • 否则从对应 type 的 Set 中移除;如果 Set 变空则从 Map 中删除该 type

offAll() ​

清空所有监听器(含通配监听器)。

ts
offAll(): void

GameEngine.destroy() 会调用此方法。

emit() ​

同步派发事件:按注册顺序依次调用匹配的监听器。

ts
emit(event: GameEvent): void

参数 ​

参数类型必填说明
eventGameEvent是待派发的事件

行为 ​

  • 派发顺序:先通配监听器(按注册顺序),再具名类型监听器
  • 监听器抛出的异常会中断后续派发(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

参数 ​

参数类型必填说明
typestring否事件类型;传 '*' 返回通配监听器数量;省略则返回全部监听器总数

返回值 ​

number —— 监听器数量。

行为 ​

  • type === undefined → 返回所有监听器(通配 + 各类型)总数
  • type === WILDCARD → 返回通配监听器数量
  • 其他 → 返回指定 type 下的监听器数量

hasListeners() ​

是否存在任意监听器。

ts
hasListeners(): boolean

EventListener 类型 ​

事件监听器函数:接收一个 GameEvent。

ts
export type EventListener = (event: GameEvent) => void

EventHandler 接口 ​

事件处理器:按 type 订阅事件,被触发时调用 handle。type 为 '*'(WILDCARD)时订阅所有事件。

ts
export interface EventHandler {
  type: string
  handle(event: GameEvent): void
}
字段/方法类型说明
typestring订阅的事件类型,'*' 表示订阅所有
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