Appearance
核心事件系统:EventEmitter
在构建任何一个有生命力的 JavaScript 应用时,我们都面临一个核心问题:如何让不同的模块之间既能高效通信,又能保持彼此的独立性,避免“牵一发而动全身”的窘境?
答案就是事件驱动(Event-Driven)架构,而其核心实现,正是我们熟知的发布-订阅模式(Publish-Subscribe Pattern)。在 mini-hammer.js 中,我们将构建一个名为 EventEmitter 的类,它将作为我们整个系统的“事件总线”和通信基石。
1. 什么是发布-订阅模式?
想象一个现实生活中的场景:
- 发布者(Publisher):一个杂志社,它会定期出版新的杂志(发布事件)。它不关心谁会来读,只管出版。
- 订阅者(Subscriber):你,我,他。我们对某个杂志感兴趣,于是去报亭“订阅”(注册回调函数)。
- 事件中心(Broker/Event Bus):报亭。它负责记录谁订阅了什么杂志,并在新杂志出版时,通知所有订阅者前来取阅。
在我们的代码中,EventEmitter 就是这个“报亭”。它允许代码的某一部分(订阅者)对某个特定的事件(如 panstart)表示兴趣,而另一部分代码(发布者)在适当的时候触发这个事件,EventEmitter 则负责通知所有订阅者执行它们注册的回调函数。
这种模式极大地降低了模块间的耦合度。发布者和订阅者互相不知道对方的存在,它们只与事件中心打交道。
2. 设计我们的 EventEmitter
一个基础的 EventEmitter 需要具备三个核心方法:
on(event, handler): 订阅一个事件。event是事件名称(字符串),handler是事件触发时要执行的回调函数。off(event, handler): 取消订阅一个事件。必须同时提供事件名和当初注册的回调函数,才能精确取消。emit(event, data): 发布(或触发)一个事件。event是事件名称,data是要传递给所有订阅者的数据对象。
3. 编码实现
让我们在 index.js 文件的 Utils 部分之后,添加 EventEmitter 类的代码。
javascript
// === EventEmitter ===
class EventEmitter {
constructor() {
// 用一个对象来存储所有的事件和对应的回调函数
// 格式: { eventName: [handler1, handler2, ...], ... }
this.events = {};
}
/**
* 订阅事件
* @param {String} event 事件名称
* @param {Function} handler 回调函数
*/
on(event, handler) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
}
/**
* 取消订阅事件
* @param {String} event 事件名称
* @param {Function} handler 回调函数
*/
off(event, handler) {
if (!this.events[event]) {
return;
}
// 从数组中找到并移除指定的回调函数
this.events[event] = this.events[event].filter(h => h !== handler);
}
/**
* 发布事件
* @param {String} event 事件名称
* @param {Object} data 传递给回调函数的数据
*/
emit(event, data) {
if (!this.events[event]) {
return;
}
// 依次调用所有订阅了该事件的回调函数
this.events[event].forEach(handler => handler(data));
}
}让我们来逐一解析这段代码:
constructor: 在构造函数中,我们初始化了一个this.events对象。它将作为我们存储所有订阅关系的“账本”。key是事件名,value是一个数组,包含了所有订阅该事件的回调函数。on(event, handler): 实现非常直接。首先检查this.events中是否已经有该事件的“账本”,如果没有,就创建一个空数组。然后,将新的handler推入这个数组即可。off(event, handler): 取消订阅稍微复杂一点。我们同样先检查“账本”是否存在。如果存在,我们使用数组的filter方法,创建一个不包含要被移除的handler的新数组,并用它覆盖掉旧的数组。这里需要注意,handler必须是当初传入on方法的同一个函数引用才能被成功移除。emit(event, data): 发布事件时,我们找到对应的“账本”数组,然后简单地遍历这个数组,并依次执行每一个handler,同时将data对象作为参数传递给它们。
4. 如何使用?
让我们来看一个简单的使用示例:
javascript
const emitter = new EventEmitter();
function onUserLogin(data) {
console.log(`欢迎回来, ${data.username}!`);
}
// 订阅 login 事件
emitter.on('login', onUserLogin);
// 在未来的某个时刻,发布 login 事件
setTimeout(() => {
emitter.emit('login', { username: 'Alex' });
}, 2000);
// 输出: (2秒后) 欢迎回来, Alex!
// 取消订阅
emitter.off('login', onUserLogin);至此,我们已经拥有了一个功能完备的事件中心。在后续的章节中,你将看到 Manager(我们的主控类)和 Recognizer(手势识别器)都将继承自 EventEmitter,从而获得发布和订阅事件的能力。这将是我们构建整个手-势识别流程的核心机制。
我们已经为我们的引擎装上了“神经系统”,下一步,我们将为它安装“感知系统”——输入适配层。