/** * 事件总线 (独立模块) * 职责: 提供模块间的事件通信机制,解耦模块依赖 * 遵循原则: 发布-订阅模式,支持事件监听和触发 * * ============================================================================ * @guide 开发指南 - EventBus 使用规范 * ============================================================================ * * 【事件命名规范】 * 使用 "模块:动作" 两段式命名,所有名称定义在 EventBus.Events 常量中: * 'game:started' 游戏开始 * 'game:finished' 游戏结束 * 'player:discarded' 玩家出牌 * 说明:框架只内置通用事件(game:/player:/room:/ui:/network: 等);具体玩法的 * 专属事件(如发牌/摸牌/吃碰杠)由子游戏自行追加到 EventBus.Events,框架不内置。 * ⚠️ 不要硬编码字符串,统一用 EventBus.Events.XXX 引用 * * 【订阅事件(在组件 init 中注册)】 * // 在 BaseComponent.init 或 Object.create 的 init 方法中: * var self = this; * var unsubscribe = EventBus.on(EventBus.Events.GAME_STARTED, function(data) { * self._onGameStarted(data); * }, self); * // 保存引用用于 destroy 时清理 * this.eventListeners['game:started'] = unsubscribe; * * 【发布事件】 * // 在控制器或业务逻辑中发布: * EventBus.emit(EventBus.Events.GAME_STARTED, { playerCount: 4 }); * EventBus.emit('player:discarded', { seat: 0, card: cardData }); * * 【一次性事件(触发后自动取消)】 * EventBus.once(EventBus.Events.GAME_FINISHED, function(data) { * self._showSettleUI(data); * }); * * 【在 destroy 中清理(防内存泄漏)】 * // 如果用 BaseComponent 的 addEventListener,destroy 时自动清理 * // 如果手动保存了 unsubscribe 函数: * if (this.eventListeners['game:started']) { * this.eventListeners['game:started'](); // 调用取消函数 * } * // 或者 BaseComponent.destroy() 会自动处理 this.eventListeners 中的所有项 * * 【推荐:通过 BaseComponent.addEventListener 代替手动管理】 * // BaseComponent 子类中: * this.addEventListener(EventBus.Events.PLAYER_DISCARDED, function(data) { * self._onPlayerDiscarded(data); * }); * // destroy() 时 BaseComponent 会自动调用所有 off */ var EventBus = { /** * 私有数据存储 * @private */ _listeners: {}, // 事件监听器列表 {eventName: [callbacks]} /** * 订阅事件 * @param {String} eventName - 事件名称 * @param {Function} callback - 回调函数 * @param {Object} context - 回调函数的上下文(this) * @return {Function} 取消订阅函数 */ on: function(eventName, callback, context) { if (!eventName || typeof callback !== 'function') { console.error('❌ EventBus.on: 无效的参数'); return null; } if (!this._listeners[eventName]) { this._listeners[eventName] = []; } var listener = { callback: callback, context: context || null }; this._listeners[eventName].push(listener); console.log('✅ EventBus: 订阅事件', eventName); // 返回取消订阅函数 var self = this; return function() { self.off(eventName, callback); }; }, /** * 订阅一次性事件(触发后自动取消订阅) * @param {String} eventName - 事件名称 * @param {Function} callback - 回调函数 * @param {Object} context - 回调函数的上下文 * @return {Function} 取消订阅函数 */ once: function(eventName, callback, context) { var self = this; var onceCallback = function() { callback.apply(context || null, arguments); self.off(eventName, onceCallback); }; return this.on(eventName, onceCallback, context); }, /** * 取消订阅事件 * @param {String} eventName - 事件名称 * @param {Function} callback - 要取消的回调函数(可选,不传则取消所有) * @return {Boolean} 是否取消成功 */ off: function(eventName, callback) { if (!this._listeners[eventName]) { return false; } // 如果没有指定callback,取消该事件的所有监听器 if (!callback) { delete this._listeners[eventName]; console.log('✅ EventBus: 取消所有', eventName, '监听器'); return true; } // 取消指定的callback var listeners = this._listeners[eventName]; for (var i = listeners.length - 1; i >= 0; i--) { if (listeners[i].callback === callback) { listeners.splice(i, 1); console.log('✅ EventBus: 取消', eventName, '监听器'); return true; } } return false; }, /** * 触发事件 * @param {String} eventName - 事件名称 * @param {*} data - 事件数据(可选) * @return {Number} 触发的监听器数量 */ emit: function(eventName, data) { if (!this._listeners[eventName]) { return 0; } var listeners = this._listeners[eventName].slice(); // 复制数组,避免迭代中修改 var count = 0; console.log('📡 EventBus: 触发事件', eventName, data); for (var i = 0; i < listeners.length; i++) { try { var listener = listeners[i]; listener.callback.call(listener.context, data); count++; } catch (error) { console.error('❌ EventBus: 事件回调异常', eventName, error); } } return count; }, /** * 检查事件是否有监听器 * @param {String} eventName - 事件名称 * @return {Boolean} 是否有监听器 */ hasListener: function(eventName) { return !!(this._listeners[eventName] && this._listeners[eventName].length > 0); }, /** * 获取事件的监听器数量 * @param {String} eventName - 事件名称 * @return {Number} 监听器数量 */ getListenerCount: function(eventName) { return this._listeners[eventName] ? this._listeners[eventName].length : 0; }, /** * 获取所有事件名称 * @return {Array} 事件名称数组 */ getEventNames: function() { var names = []; for (var name in this._listeners) { if (this._listeners.hasOwnProperty(name)) { names.push(name); } } return names; }, /** * 清空所有事件监听器 */ clear: function() { this._listeners = {}; console.log('✅ EventBus: 已清空所有监听器'); }, /** * 清空指定事件的所有监听器 * @param {String} eventName - 事件名称 */ clearEvent: function(eventName) { if (this._listeners[eventName]) { delete this._listeners[eventName]; console.log('✅ EventBus: 已清空', eventName, '的所有监听器'); } } }; /** * 事件名称常量容器(框架不预置任何具体事件名) * * 框架保持「游戏中立」:只提供发布-订阅机制与一个空的 EventBus.Events 容器, * 不预定义任何事件名称常量。各子游戏在自身事件常量文件(如 * game/constants/GameEvents.js)中向 EventBus.Events 注册自己用到的【全部】事件, * 业务侧统一通过 EventBus.Events.XXX 引用。 * * 命名规范:两段式 "模块:动作"。例如通用语义 'game:started' / 'player:discarded' / * 'room:updated' / 'ui:sceneChanged' / 'network:connected';玩法专属自定前缀(如 * 'mahjong:tilesDealt')。统一用常量引用,不在业务中硬编码字符串。 */ EventBus.Events = {};