Files
youle_framework/client/js/gameabc-framework/system/EventBus.js
T
2026-08-19 08:14:48 +08:00

235 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 事件总线 (独立模块)
* 职责: 提供模块间的事件通信机制,解耦模块依赖
* 遵循原则: 发布-订阅模式,支持事件监听和触发
*
* ============================================================================
* @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 = {};