EventEmitter
基础知识
EventEmitter
,即事件发射器,是一个实现了“发布-订阅”设计模式的类。这个模式允许我们定义一种一对多的依赖关系,当一个对象(发布者)的状态发生改变时,所有依赖于它的对象(订阅者)都会得到通知并自动更新。
发布者 (Publisher): 也称为“事件发射器”,是事件的中心枢纽。它负责维护事件列表,并在适当时机“发布”或“发射”事件。
订阅者 (Subscriber): 也称为“事件监听器”(Listener),它是一个函数。它向发布者“订阅”自己感兴趣的特定事件。
主题/事件 (Topic/Event): 一个用于区分不同消息类型的标签,通常是一个字符串(如
'click'
,'data'
,'error'
)。订阅者订阅的是特定的事件,发布者发布的也是特定的事件。
EventEmitter
的核心 API 包括:
on(eventName, listener)
: 订阅一个事件。emit(eventName, ...args)
: 发布一个事件。off(eventName, listener)
: 取消订阅一个事件。once(eventName, listener)
: 订阅一个只执行一次的事件。
核心思路
我们的目标是创建一个 EventEmitter
类。其核心思路是内部维护一个“事件中心”,用于存储所有事件和其对应的监听器。
事件中心的数据结构: 最合适的结构是一个字典(或
Map
),其中:- 键 (key) 是事件名称 (
eventName
) 的字符串。 - 值 (value) 是一个数组,包含了所有订阅了该事件的监听器函数(
listener
)。
// 伪代码 this._events = { event1: [listenerA, listenerB], event2: [listenerC], };
- 键 (key) 是事件名称 (
on(eventName, listener)
的逻辑:- 检查事件中心里是否已经有
eventName
这个键。 - 如果没有,就创建一个新数组作为值:
this._events[eventName] = []
。 - 将
listener
函数推入(push
)到这个数组中。
- 检查事件中心里是否已经有
emit(eventName, ...args)
的逻辑:- 查找事件中心里
eventName
对应的监听器数组。 - 如果找到了,就遍历这个数组,并依次执行其中的每一个监听器函数,同时将
...args
参数传递给它们。
- 查找事件中心里
off(eventName, listener)
的逻辑:- 查找
eventName
对应的监听器数组。 - 如果找到了,就从数组中找到并移除指定的
listener
函数。
- 查找
once(eventName, listener)
的逻辑:- 这是一个巧妙的封装。我们可以创建一个临时的“包装函数”。
- 在这个包装函数内部,首先执行原始的
listener
,然后立即调用off
方法将这个包装函数自身从事件中移除。 - 最后,使用
on
方法订阅这个包装函数。
代码实现
下面是一个包含核心功能的 EventEmitter
类的完整实现。
/**
* 实现一个 EventEmitter 类
*/
class EventEmitter {
constructor() {
// 1. 初始化事件中心,用于存储事件和监听器
this._events = {};
}
/**
* 订阅事件
* @param {string} eventName - 事件名称
* @param {Function} listener - 监听器函数
*/
on(eventName, listener) {
if (!this._events[eventName]) {
this._events[eventName] = [];
}
this._events[eventName].push(listener);
return this; // 支持链式调用
}
/**
* 发布事件
* @param {string} eventName - 事件名称
* @param {...any} args - 传递给监听器的参数
*/
emit(eventName, ...args) {
const listeners = this._events[eventName];
if (!listeners || listeners.length === 0) {
return false; // 如果没有监听器,则直接返回
}
// 2. 创建一个 listeners 数组的副本进行遍历,防止在 emit 过程中有 off 操作导致数组塌陷
const listenersCopy = [...listeners];
listenersCopy.forEach((listener) => {
listener(...args);
});
return true;
}
/**
* 取消订阅事件
* @param {string} eventName - 事件名称
* @param {Function} listener - 要移除的监听器函数
*/
off(eventName, listener) {
const listeners = this._events[eventName];
if (!listeners || listeners.length === 0) {
return this;
}
// 3. 使用 filter 过滤掉要移除的监听器
this._events[eventName] = listeners.filter((l) => l !== listener);
return this;
}
/**
* 订阅一个只执行一次的事件
* @param {string} eventName - 事件名称
* @param {Function} listener - 监听器函数
*/
once(eventName, listener) {
// 4. 创建一个包装函数
const onceWrapper = (...args) => {
// 在执行原始监听器后,立即移除自身
listener(...args);
this.off(eventName, onceWrapper);
};
// 订阅这个包装函数
this.on(eventName, onceWrapper);
return this;
}
}
代码解析
constructor 初始化:
- 初始化
_events
属性为一个空对象,用于存储事件与监听器的映射关系。 - 下划线前缀
_
是约定俗成的内部属性标识,提示外部代码不应直接访问该属性。
- 初始化
emit 中的副本遍历:
- 在
emit
方法中,通过const listenersCopy = [...listeners]
创建监听器数组的副本。 - 若直接遍历原始数组,当某个监听器执行
off
操作移除同事件的其他监听器时,会导致遍历过程中数组结构改变,可能跳过部分监听器。 - 遍历副本可确保本次
emit
触发时所有应调用的监听器都被正确执行,避免因数组动态修改引发的逻辑错误。
- 在
off 中的 filter 过滤:
- 使用
Array.prototype.filter
方法移除指定监听器,是最简洁安全的实现方式。 - 该方法会创建一个新数组,过滤掉与目标监听器引用相同的项,再用新数组覆盖原数组,完成监听器的移除操作,确保数组操作的原子性和准确性。
- 使用
once 中的 onceWrapper 包装函数:
once
方法通过创建onceWrapper
包装函数实现单次监听逻辑。- 当
onceWrapper
被首次调用时,先执行原始监听器listener
并传递参数,随后立即调用this.off(eventName, onceWrapper)
从监听器数组中移除自身。 - 这种“自销毁”机制确保原始监听器仅在事件首次触发时执行,后续相同事件触发时不再响应,无需额外状态管理,实现简洁且高效。