In the observer pattern (also commonly known as the publish-subscribe model), we can observe/subscribe to events emitted by publishers and execute code whenever an event happens.
Implement an EventEmitter class similar to the one in Node.js that follows such an observer pattern.
Example usage of the EventEmitter class:
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"
EventEmitter APIImplement the following classes and APIs:
new EventEmitter()Creates an instance of the EventEmitter class. Events and listeners are isolated within the EventEmitter instances they're added to, aka listeners shouldn't react to events emitted by other EventEmitter instances.
emitter.on(eventName, listener)Adds a callback function (listener) that will be invoked when an event with the name eventName is emitted.
| Parameter | Type | Description |
|---|---|---|
eventName | string | The name of the event. |
listener | Function | The callback function to be invoked when the event occurs. |
Returns the EventEmitter instance so that calls can be chained.
emitter.off(eventName, listener)Removes the specified listener from the list of listeners for the event with the name eventName.
| Parameter | Type | Description |
|---|---|---|
eventName | string | The name of the event. |
listener | Function | Callback function to be removed from the list of listeners for the event. |
Returns the EventEmitter instance so that calls can be chained.
emitter.emit(eventName[, ...args])Invokes each of the listeners listening to eventName with the supplied arguments in order.
| Parameter | Type | Description |
|---|---|---|
eventName | string | The name of the event. |
...args | any | Arguments to invoke the list of listener functions with. |
Returns true if the event had listeners, false otherwise.
An event-based interaction model is the most common way of building user interfaces. The DOM is also built around this model with the document.addEventListener() and document.removeEventListener() APIs to allow responding to events like click, hover, input, etc.
The following are good questions to ask the interviewer to demonstrate your thoughtfulness. Depending on their response, you might need to adjust the implementation accordingly.
emitter.emit() be called without any arguments besides the eventName?
eventName?
eventName is emitted in the order they were added.emitter.off() is being called once for that listener?
this value of the listeners be?
null.emitter.emit()?
We will handle all the above cases except for the last two cases.
The clean default is a map from eventName to an array of listeners.
Conceptually:
events = {foo: [Function1, Function3],bar: [Function2],};
That structure makes all three operations direct:
on() appends to one event bucketoff() searches inside one event bucketemit() reads one event bucket and iterates it in orderA flat list of { eventName, listener } pairs also works, but it forces every off() and emit() call to scan unrelated entries.
Because eventName comes from user input, the implementation stores these buckets in Object.create(null) instead of a normal object literal. That avoids collisions with inherited keys like toString.
EventEmitter.on()Create the listener array if this is the first time the event name appears, then push the new listener and return this for chaining.
EventEmitter.off()If the event does not exist, do nothing. Otherwise, find the first matching listener with findIndex() and remove only that single occurrence with splice().
Removing one occurrence matters because the same function can be registered multiple times and should be removed in registration order.
EventEmitter.emit()If the event does not exist or has no listeners, return false.
Otherwise, clone the listener array with slice() and call each listener with the emitted arguments. Cloning protects the current emission from listeners that add or remove handlers while emit() is running.
eventNamesIf you store listeners in a plain JavaScript object, user-provided names such as valueOf or toString can collide with inherited properties.
const emitter = new EventEmitter();emitter.emit('toString');
Two common fixes are:
Map instead of an object. This is the modern approach.Object.create(null) so that the object does not have a prototype and no additional properties.export default class EventEmitter {constructor() {// 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}*/on(eventName, listener) {// Group listeners by event name so emit/off only touch one bucket.if (!Object.hasOwn(this._events, eventName)) {this._events[eventName] = [];}this._events[eventName].push(listener);return this;}/*** @param {string} eventName* @param {Function} listener* @returns {EventEmitter}*/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;}/*** @param {string} eventName* @param {...unknown} args* @returns {boolean}*/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;}}
interface EventEmitter {_events: Record<string, Array<Function>>;on(eventName: string, listener: Function): EventEmitter;off(eventName: string, listener: Function): EventEmitter;emit(eventName: string, ...args: Array<any>): boolean;}export default function EventEmitter(this: EventEmitter) {// Avoid creating objects via `{}` to exclude unwanted properties// on the prototype (such as `.toString`).this._events = Object.create(null);}EventEmitter.prototype.on = function (this: EventEmitter,eventName: string,listener: Function,): EventEmitter {// Group listeners by event name so emit/off only touch one bucket.if (!Object.hasOwn(this._events, eventName)) {this._events[eventName] = [];}this._events[eventName].push(listener);return this;};EventEmitter.prototype.off = function (this: EventEmitter,eventName: string,listener: Function,): EventEmitter {// 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;};EventEmitter.prototype.emit = function (this: EventEmitter,eventName: string,...args: Array<any>): boolean {// 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() is called without any arguments.eventNames.eventNames are built-in object properties like valueOf, toString.this, because arrow functions do not get their own method receiver.EventEmitter supports more features, such as symbol event names, but they are intentionally out of scope here.console.log() aparecerão aqui.