235 lines
7.9 KiB
JavaScript
235 lines
7.9 KiB
JavaScript
/**
|
||
* 事件总线 (独立模块)
|
||
* 职责: 提供模块间的事件通信机制,解耦模块依赖
|
||
* 遵循原则: 发布-订阅模式,支持事件监听和触发
|
||
*
|
||
* ============================================================================
|
||
* @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 = {};
|