在 观察者模式(也常被称为发布-订阅模型)中,我们可以观察/订阅由发布者发出的事件,并在发生事件时执行代码。
实现一个类似于 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
。
在 观察者模式(也常被称为发布-订阅模型)中,我们可以观察/订阅由发布者发出的事件,并在发生事件时执行代码。
实现一个类似于 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()
语句将显示在此处。