完整课程入口:996 全套课程体系 | Lua 学习路径 | 幂尔框架 mirs.cn
业务初期,模块之间直接互相调用:充值成功后,充值模块去调背包加道具、调公告、调成就统计。三个月后加"充值返利",又得改充值模块。问题不在业务,在于依赖方向错了——上游模块不应该知道下游有谁。
事件总线(EventBus)的解法:模块只负责"发布事件",不关心谁在听;关心者自行"订阅事件"。充值模块只发一条 RECHARGE_OK,背包、公告、返利各自监听处理。新增需求 = 新增订阅者,上游零改动。
local Bus = { listeners = {} }
function Bus.on(event, fn)
Bus.listeners[event] = Bus.listeners[event] or {}
table.insert(Bus.listeners[event], fn)
end
function Bus.emit(event, ...)
local list = Bus.listeners[event]
if not list then return end
for _, fn in ipairs(list) do fn(...) end
end
-- 充值模块只发事件
Bus.emit("RECHARGE_OK", playerId, amount)
-- 各模块各自订阅
Bus.on("RECHARGE_OK", GiveItems)
Bus.on("RECHARGE_OK", SendAnnounce)
事件名集中定义。 用一张常量表管理事件名,防止"RechargeOk / RECHARGE_OK / recharge_ok"三套拼写并存——字符串事件名裸写是事件系统腐化的开始。
订阅必须能反注册。 Bus.off(event, fn) 与 on 成对存在,界面销毁、对象删除时调用。否则监听闭包抓着已销毁的界面引用,轻则空指针报错,重则内存泄漏。上一轮讲过的闭包泄漏,事件总线是最常见的泄漏源头。
处理器内不得抛错炸全场。 emit 时用 pcall 包住每个监听器,单个处理器出错记日志、继续执行其余监听,保证一个模块的 bug 不阻断整条事件链。
同步为默认,异步要显式。 emit 是同步派发,处理器立即执行;需要延迟/分帧的用 emitLater 走协程或定时器。混用而不标记,时序问题将极难排查。
事件总线不是银弹:事件链一长,"谁动了数据"会变难查。约定两条线——强依赖、必达的逻辑走直接调用;广播性质、可有可无的副作用走事件。把事件名、参数结构写进文档,事件总线才是解耦神器而不是谜团制造机。