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"
Implement the following 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.
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"
Implement the following 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.
Firstly, we have to decide on the data structure to store the events and the listeners. We can either use:
eventName
s to an array of listener functions.events = {foo: [Function1, Function3],bar: [Function2],};
eventName
.eventName
is provided by the user, it can be any value and might conflict with existing keys on Object.prototype
such as toString
. We will handle this situation.eventName
and listener pairs.events = [{ eventName: 'foo', listener: Function1 },{ eventName: 'bar', listener: Function2 },{ eventName: 'foo', listener: Function3 },];
emit()
and off()
operations will require iterating through the array, you can't instantly determine if an event exists and ignore emission of non-existent events.eventName
strings.Approach #1 is clearly superior, so we will use that. To mitigate the issue of user-provided eventName
s conflicting with keys on Object.prototype
, we can instantiate the _events
object with Object.create(null)
or use a ES6 Map
class.
EventEmitter.on()
Implementing EventEmitter.on()
is pretty straightforward. Firstly check if eventName
is present as a key of the _events
object and make the value an empty array (for the list of listeners for that event) if it is the first time this eventName
is encountered. Then push the listener
into the array.
Return this
so that the method can be chained.
EventEmitter.off()
First check if eventName
is present as a key of the _events
object. If no events with eventName
exists, we do not need to proceed further and can do an early return
.
Since we only want to remove the first instance of any matching listener
, we'll use listeners.findIndex()
and remove only one instance via .splice()
, instead of using something like .filter()
which will remove all matching instances.
Return this
so that the method can be chained.
EventEmitter.emit()
Check if the eventName
exists or has any events and we can terminate and return false
if the eventName
doesn't exist or if there are no listeners for eventName
.
To pass the rest of the arguments to each listener, we have to use ...args
in the method signature to capture all other arguments as the variable args
. The listeners can be called with args
via Function.prototype.apply()
or Function.prototype.call()
.
eventName
sAs mentioned above, if you're using a plain JavaScript object to map eventName
to callbacks, one potential issue is using eventName
s that clash with properties existing on JavaScript objects such as valueOf
and toString
.
const emitter = new EventEmitter();emitter.emit('toString'); // Might crash because the property does exist on the object.
Two ways to handle this:
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);}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()
is called without any arguments.eventName
s.eventName
s are built-in object properties like valueOf
, toString
.this
context, hence they should not be used as methods on objects as the this
will not be referring to the object. Thus the emitter.on()
and emitter.off()
methods cannot be defined as an arrow function if the return value is the this
object.EventEmitter
's implementation allows eventName
s to be symbols which we don't allow here.console.log()
statements will appear here.