在 观察者模式(也常被称为发布-订阅模型)中,我们可以观察/订阅由发布者发出的事件,并在发生事件时执行代码。
实现一个类似于 Node.js 中的 EventEmitter 类,该类遵循这种观察者模式。
EventEmitter 类的使用示例:
const emitter = new EventEmitter();function addTwoNumbers(a, b) {console.log(`The sum is ${a + b}`);}emitter.on('foo', addTwoNumbers);emitter.emit('foo', 2, 5);// > "The sum is 7"emitter.on('foo', (a, b) => console.log(`The product is ${a * b}`));emitter.emit('foo', 4, 5);// > "The sum is 9"// > "The product is 20"emitter.off('foo', addTwoNumbers);emitter.emit('foo', -3, 9);// > "The product is -27"
实现以下 API:
new EventEmitter()创建 EventEmitter 类的实例。事件和侦听器在它们被添加到的 EventEmitter 实例中是隔离的,即侦听器不应该对其他 EventEmitter 实例发出的事件做出反应。
emitter.on(eventName, listener)添加一个回调函数 (listener),当发出名称为 eventName 的事件时,将调用该函数。
| 参数 | 类型 | 描述 |
|---|---|---|
eventName | string | 事件的名称。 |
listener | Function | 发生事件时要调用的回调函数。 |
返回 EventEmitter 实例,以便可以链接调用。
emitter.off(eventName, listener)从名称为 eventName 的事件的侦听器列表中删除指定的 listener。
| 参数 | 类型 | 描述 |
|---|---|---|
eventName | string | 事件的名称。 |
listener | Function | 要从事件的侦听器列表中删除的回调函数。 |
返回 EventEmitter 实例,以便可以链接调用。
emitter.emit(eventName[, ...args])按顺序调用每个侦听 eventName 的侦听器,并提供提供的参数。
| 参数 | 类型 | 描述 |
|---|---|---|
eventName | string | 事件的名称。 |
...args | any | 用于调用侦听器函数列表的参数。 |
如果事件有侦听器,则返回 true,否则返回 false。
基于事件的交互模型是构建用户界面的最常见方式。 DOM 也是围绕此模型构建的,使用 document.addEventListener() 和 document.removeEventListener() API 来允许响应 click、hover、input 等事件。
以下是向面试官提问以展示您的周全考虑的好问题。 根据他们的回答,您可能需要相应地调整实现。
eventName 之外,可以不带任何参数调用 emitter.emit() 吗?
eventName 多次添加同一个监听器吗?
eventName 被触发时,它将被调用一次,对于每次添加它的顺序。emitter.off() 被调用一次,那么会发生什么?
this 值应该是什么?
null。emitter.emit() 期间抛出错误怎么办?
我们将处理以上所有情况,除了最后两种情况。
首先,我们必须确定用于存储事件和监听器的数据结构。 我们可以使用:
eventName 映射到监听器函数数组。events = {foo: [Function1, Function3],bar: [Function2],};
eventName 的监听器列表。eventName 由用户提供,它可以是任何值,并且可能与 Object.prototype 上的现有键(例如 toString)冲突。 我们将处理这种情况。eventName 和监听器对的扁平数组。events = [{ eventName: 'foo', listener: Function1 },{ eventName: 'bar', listener: Function2 },{ eventName: 'foo', listener: Function3 },];
emit() 和 off() 操作将需要遍历数组,你无法立即确定事件是否存在并忽略不存在的事件的触发。eventName 字符串,可能需要更多空间来存储数据。方法 #1 显然更好,所以我们将使用它。 为了缓解用户提供的 eventName 与 Object.prototype 上的键冲突的问题,我们可以使用 Object.create(null) 实例化 _events 对象或使用 ES6 Map 类。
EventEmitter.on()实现 EventEmitter.on() 非常简单。 首先检查 eventName 是否作为 _events 对象的键存在,如果这是第一次遇到此 eventName,则将该值设为空数组(用于该事件的监听器列表)。 然后将 listener 推入数组。
返回 this,以便可以链接该方法。
EventEmitter.off()首先检查 eventName 是否作为 _events 对象的键存在。如果不存在任何带有 eventName 的事件,我们不需要继续进行,可以直接 return。
由于我们只想删除任何匹配 listener 的第一个实例,我们将使用 listeners.findIndex() 并通过 .splice() 仅删除一个实例,而不是使用类似 .filter() 的方法,后者将删除所有匹配的实例。
返回 this,以便可以链接该方法。
EventEmitter.emit()检查 eventName 是否存在或是否有任何事件,如果 eventName 不存在或没有 eventName 的监听器,我们可以终止并返回 false。
为了将剩余的参数传递给每个监听器,我们必须在方法签名中使用 ...args 来捕获所有其他参数作为变量 args。监听器可以使用 args 通过 Function.prototype.apply() 或 Function.prototype.call() 调用。
eventName 冲突如上所述,如果您使用普通的 JavaScript 对象将 eventName 映射到回调,一个潜在的问题是使用与 JavaScript 对象上存在的属性(如 valueOf 和 toString)冲突的 eventName。
const emitter = new EventEmitter();emitter.emit('toString'); // 可能会崩溃,因为该属性确实存在于对象上。
两种处理方法:
Map 而不是对象。这是现代方法。Object.create(null) 创建您的普通 JavaScript 对象,这样对象就没有原型,也没有其他属性。export default class EventEmitter {constructor() {// Avoid creating objects via `{}` to exclude unwanted properties// on the prototype (such as `.toString`).this._events = Object.create(null);}on(eventName, listener) {if (!Object.hasOwn(this._events, eventName)) {this._events[eventName] = [];}this._events[eventName].push(listener);return this;}off(eventName, listener) {// Ignore non-existing eventNames.if (!Object.hasOwn(this._events, eventName)) {return this;}const listeners = this._events[eventName];// Find only first instance of the listener.const index = listeners.findIndex((listenerItem) => listenerItem === listener,);if (index < 0) {return this;}this._events[eventName].splice(index, 1);return this;}emit(eventName, ...args) {// Return false for non-existing eventNames or events without listeners.if (!Object.hasOwn(this._events, eventName) ||this._events[eventName].length === 0) {return false;}// Make a clone of the listeners in case one of the listeners// mutates this listener array.const listeners = this._events[eventName].slice();listeners.forEach((listener) => {listener.apply(null, args);});return true;}}
export default function EventEmitter() {// Avoid creating objects via `{}` to exclude unwanted properties// on the prototype (such as `.toString`).this._events = Object.create(null);}/*** @param {string} eventName* @param {Function} listener* @returns {EventEmitter}*/EventEmitter.prototype.on = function (eventName, listener) {if (!Object.hasOwn(this._events, eventName)) {this._events[eventName] = [];}this._events[eventName].push(listener);return this;};/*** @param {string} eventName* @param {Function} listener* @returns {EventEmitter}*/EventEmitter.prototype.off = function (eventName, listener) {// Ignore non-existing eventNames.if (!Object.hasOwn(this._events, eventName)) {return this;}const listeners = this._events[eventName];// Find only first instance of the listener.const index = listeners.findIndex((listenerItem) => listenerItem === listener,);if (index < 0) {return this;}this._events[eventName].splice(index, 1);return this;};/*** @param {string} eventName* @param {...any} args* @returns {boolean}*/EventEmitter.prototype.emit = function (eventName, ...args) {// Return false for non-existing eventNames or events without listeners.if (!Object.hasOwn(this._events, eventName) ||this._events[eventName].length === 0) {return false;}// Make a clone of the listeners in case one of the listeners// mutates this listener array.const listeners = this._events[eventName].slice();listeners.forEach((listener) => {listener.apply(null, args);});return true;};
emitter.emit() 在没有任何参数的情况下被调用。eventName 调用。eventName 是内置对象属性,如 valueOf、toString。this 上下文的词法作用域,因此它们不应该用作对象的方法,因为 this 不会引用该对象。 因此,如果返回值是 this 对象,则 emitter.on() 和 emitter.off() 方法不能定义为箭头函数。EventEmitter 的实现允许 eventName 为符号,而我们在这里不允许。console.log() 语句将显示在此处。