Skip to content

Instantly share code, notes, and snippets.

@copperwall
Last active February 11, 2019 03:42
Show Gist options
  • Save copperwall/67c1ebaa2581a86e177356c68390970c to your computer and use it in GitHub Desktop.
Save copperwall/67c1ebaa2581a86e177356c68390970c to your computer and use it in GitHub Desktop.
First shot at an EventEmitter implementation following the NodeJS Events API docs
// Specs
// emits newListener when new listeners are added
// event contains event name and reference to new listener
// emits removeListener when existing listeners are removed
//
const DEFAULT_MAX_LISTENERS = 10;
const PREPEND = 'prepend';
const APPEND = 'append';
class EventEmitter {
constructor() {
this.maxListeners = DEFAULT_MAX_LISTENERS;
// Could be all ES6 and use a Map or WeakMap.
this.events = {};
}
// Concerns
// A failing handler should trigger error event if it exists, otherwise throw global error.
// It synchronously calls the listeners.
emit(eventName, ...args) {
const listeners = this.events[eventName] || [];
if (listeners.length === 0) {
return false;
}
for (let listener of listeners) {
try {
listener.apply(this, args);
} catch (e) {
if (!this.emit('error', e)) {
// If know error listeners, throw global error.
throw e;
}
}
}
return true;
}
eventNames() {
return Object.keys(this.events);
}
getMaxListeners() {
return this.maxListeners;
}
listenerCount(eventName) {
if (this.events[eventName] === undefined) {
return 0;
}
return this.events[eventName].length;
}
listeners(eventName) {
if (this.events[eventName] === undefined) {
return [];
}
return this.events[eventName].map(listener => listener.listener || listener);
}
_addListener(eventName, listener, prependOrAppend) {
if (typeof listener !== 'function') {
throw new TypeError('listener must be of type function');
}
const listeners = this.events[eventName] || [];
this.emit('newListener', listener);
if (prependOrAppend === APPEND) {
listeners.push(listener);
} else if (prependOrAppend === PREPEND) {
listeners.unshift(listener);
} else {
throw new TypeError('prependOrAppend must be of type PREPEND or APPEND');
}
this.events[eventName] = listeners;
return this;
}
// Emits newListener event and then adds listener to the given event name.
addListener(eventName, listener) {
return this._addListener(eventName, listener, APPEND);
}
once(eventName, listener) {
const onceRunner = (...args) => {
this.removeListener(eventName, listener);
listener.apply(this, args);
};
onceRunner.listener = listener;
this.addListener(eventName, onceRunner);
return this;
}
prerendListener(eventName, listener) {
return this._addListener(eventName, listener, PREPEND);
}
prependOnceListener(eventName, listener) {
const onceRunner = (...args) => {
this.removeListener(eventName, listener);
listener.apply(this, args);
};
onceRunner.listener = listener;
this.prerendListener(eventName, onceRunner);
return this;
}
removeAllListeners(eventName) {
let events = eventName !== undefined ?
[eventName] :
Object.keys(this.events);
events.forEach(event => {
this.listeners(event).forEach(
listener => this.removeListener(event, listener)
);
delete this.events[eventName];
});
return this;
}
/**
* NOTE: only removes one instance of listener. Can't use filter.
* Removes most recently added instance.
* @param {*} eventName
* @param {*} listener
*/
removeListener(eventName, listener) {
const listeners = this.listeners(eventName);
const listenerPos = listeners.lastIndexOf(listener);
// Remove the index in the actual raw listener list.
if (listenerPos >= 0) {
this.events[eventName].splice(listenerPos, 1);
}
this.emit('removeListener', listener);
return this;
}
setMaxListeners(n) {
this.maxListeners = n;
}
rawListeners(eventName) {
return this.events[eventName];
}
}
// Aliases
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
module.exports = EventEmitter;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment