EventEmitter
什么是EventEmitter?
EventEmitter(事件派发器)是一个对事件进行监听的对象,简单来说就是为事件写回调函数,当触发一个事件执行后,会执行为该事件绑定的回调函数。
JavaScript 事件最核心的包括事件监听 (addListener)、事件触发 (emit)、事件删除 (removeListener),理解如下:
考虑一个DOM事件:
// Typescript
const button = document.querySelector('button');
button.addEventListener("click", (event) => {
// do something with the event
})我们向按钮单击事件添加了一个listener (监听器),并且已经订阅了一个正在被发出的事件,当事件发生时会触发回调。每次单击该按钮时,都会发出该事件,而该事件会触发回调。
当处理现有代码库时,或许需要触发自定义事件。不像单击按钮这样的特定DOM事件,而是假设想基于其他触发器发出一个事件,并得到一个事件响应。我们需要一个自定义事件派发器来实现这一点。
事件派发器是一种模式,它监听一个已命名的事件,触发回调,然后发出该事件并附带一个值。有时这被称为“发布/订阅”模型或监听器。它们指的是同一件事。
发布订阅模式
发布 — 订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状 态发生改变时,所有依赖于它的对象都将得到通知。
在 JavaScript开发中,我们一般用事件模型来替代传统的发布 — 订阅模式。
实现的关键要素
- 发布者有一个订阅者缓存队列
- 发布者有增加和删除订阅者的方法
- 发布者状态改变,需要notify方法通知队列中的所有订阅者
- js中采用事件回调的方式来更新订阅者,因此订阅者不再需要update方法
下面来模拟下EventEmitter的初步实现,模拟vue中的Event bus
class EventEmitter {
constructor() {
this._events = {};
}
// 添加事件监听
on(eventName, cb) {
if (typeof cb !== "function") {
return;
}
if (!this._events[eventName]) {
this._events[eventName] = [];
}
this._events[eventName].push(cb);
}
// 触发事件
emit(eventName, ...args) {
this._events[eventName].forEach((item) => {
item.apply(this, args);
});
}
off(eventName, fn) {
// 只传事件名,即删除该事件类型的所有事件函数
if (!fn) {
this._events[eventName] = null;
delete this._events[eventName];
return this;
}
const index = this._events[eventName.findIndex((cb) => cb === fn);
if (index !== -1) {
this._events[eventName].splice(index, 1);
}
return this;
}
}once函数
EventEmitter中的once方法可以做到绑定的事件只调用一次,之后不会再被调用,他的实现方式实在怎么样的?正常情况应该是在回调函数被调用一次之后移除这个回调。可以考虑在回调函数上加上once属性,在发起回调的时候判断once是否为真,来确定是否移除这个回调。这样可以达到目的,但是在发起回调时,需要每一次都判断,给通知方法增加了额外的负担,来考虑一个更聪明的实现方式。
once(eventName, cb) {
const wrap = (...args) => {
cb.apply(this, args);
this.off(eventName, wrap);
};
wrap.cb = cb;
this.on(eventName, wrap);
return this; // 返回自身,方便链式调用
}将回调函数包裹起来,在包裹函数内部移除原回调函数,然后将wrap函数添加进观察者队列。同时要将原回调函数存进wrap中,用在在移除原回调时判断。
修改off方法
off(eventName, fn) {
// 只传事件名,即删除该事件类型的所有事件函数
if (!fn) {
this._events[eventName] = null;
delete this._events[eventName];
return this;
}
const cbs = this._events[eventName];
const index = this._events[eventName].findIndex((cb) => {
return cb === fn || cb.fn === fn;
});
if (index !== -1) {
this._events[eventName].splice(index, 1);
}
return this;
}修改emit方法
emit(eventName, ...args) {
// 浅拷贝 存放数组副本,预防off后数组发生变化,导致调用错误
const listeners = [...this._events[eventName]];
if (!listeners) return;
listeners.forEach((item) => {
item.apply(this, args);
});
}newListener事件
比较有趣的是EventEmitter同时提供了newListener事件,每次添加观察者(即使是第二次添加newListener)时都会触发这个事件,在on方法中需要添加如下代码:
on(eventName, cb) {
this.emit("newListener", eventName, cb);
if (typeof cb !== "function") {
return;
}
if (!this._events[eventName]) {
this._events[eventName] = [];
}
this._events[eventName].push(cb);
}若在 'newListener' 的回调中再次注册新监听器,会形成递归调用。源码通过临时标记防止无限循环:
constructor() {
this._events = {};
this._emitNewListener = false;
}
on(eventName, cb) {
if (this._events.newListener && !this._emitNewListener) {
this._emitNewListener = true; // 设置防递归标记
this.emit("newListener", eventName, cb);
this._emitNewListener = false; // 清除标记
}
if (typeof cb !== "function") {
return;
}
if (!this._events[eventName]) {
this._events[eventName] = [];
}
this._events[eventName].push(cb);
}