-
-
Save mudge/5830382 to your computer and use it in GitHub Desktop.
/* Polyfill indexOf. */ | |
var indexOf; | |
if (typeof Array.prototype.indexOf === 'function') { | |
indexOf = function (haystack, needle) { | |
return haystack.indexOf(needle); | |
}; | |
} else { | |
indexOf = function (haystack, needle) { | |
var i = 0, length = haystack.length, idx = -1, found = false; | |
while (i < length && !found) { | |
if (haystack[i] === needle) { | |
idx = i; | |
found = true; | |
} | |
i++; | |
} | |
return idx; | |
}; | |
}; | |
/* Polyfill EventEmitter. */ | |
var EventEmitter = function () { | |
this.events = {}; | |
}; | |
EventEmitter.prototype.on = function (event, listener) { | |
if (typeof this.events[event] !== 'object') { | |
this.events[event] = []; | |
} | |
this.events[event].push(listener); | |
}; | |
EventEmitter.prototype.removeListener = function (event, listener) { | |
var idx; | |
if (typeof this.events[event] === 'object') { | |
idx = indexOf(this.events[event], listener); | |
if (idx > -1) { | |
this.events[event].splice(idx, 1); | |
} | |
} | |
}; | |
EventEmitter.prototype.emit = function (event) { | |
var i, listeners, length, args = [].slice.call(arguments, 1); | |
if (typeof this.events[event] === 'object') { | |
listeners = this.events[event].slice(); | |
length = listeners.length; | |
for (i = 0; i < length; i++) { | |
listeners[i].apply(this, args); | |
} | |
} | |
}; | |
EventEmitter.prototype.once = function (event, listener) { | |
this.on(event, function g () { | |
this.removeListener(event, g); | |
listener.apply(this, arguments); | |
}); | |
}; |
What does var i, listeners, length, args = [].slice.call(arguments, 1);
do? Why is that there?
Never mind, I just saw the commas and realized it is just initializing the variables except for the last args
which is set to all the arguments of the function minus the event name.
This is awesome!!!
Borrowing from this, I wrote the eventify
function which will turn any object into an event emitter:
const eventify = (self) => {
self.events = {}
self.on = function (event, listener) {
if (typeof self.events[event] !== 'object') {
self.events[event] = []
}
self.events[event].push(listener)
}
self.removeListener = function (event, listener) {
let idx
if (typeof self.events[event] === 'object') {
idx = self.events[event].indexOf(listener)
if (idx > -1) {
self.events[event].splice(idx, 1)
}
}
}
self.emit = function (event) {
var i, listeners, length, args = [].slice.call(arguments, 1);
if (typeof self.events[event] === 'object') {
listeners = self.events[event].slice()
length = listeners.length
for (i = 0; i < length; i++) {
listeners[i].apply(self, args)
}
}
}
self.once = function (event, listener) {
self.on(event, function g () {
self.removeListener(event, g)
listener.apply(self, arguments)
})
}
}
It can be used like:
const myEmitter = {}
eventify(myEmitter)
Using lodash and es6
function eventMixin(obj) {
obj._events = {};
obj.on = (event, listener) => {
if (_.isNil(obj._events[event])) {
obj._events[event] = [];
}
obj._events[event].push(listener);
};
obj.emit = (event, ...args) => {
if (_.isNil(obj._events[event])) {
return;
}
_.forEach(obj._events[event], (listener) => {
listener.apply(obj, args);
});
};
obj.removeListener = (event, listener) => {
if (_.isNil(obj._events[event])) {
return;
}
_.pull(obj._events[event], listener);
};
obj.once = (event, listener) => {
obj.on(event, function handler(...args) {
obj.removeListener(event, handler);
listener.apply(obj, args);
});
};
};
Using ES2015
classes...
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (typeof this.events[event] !== 'object') {
this.events[event] = [];
}
this.events[event].push(listener);
return () => this.removeListener(event, listener);
}
removeListener(event, listener) {
if (typeof this.events[event] === 'object') {
const idx = this.events[event].indexOf(listener);
if (idx > -1) {
this.events[event].splice(idx, 1);
}
}
}
emit(event, ...args) {
if (typeof this.events[event] === 'object') {
this.events[event].forEach(listener => listener.apply(this, args));
}
}
once(event, listener) {
const remove = this.on(event, (...args) => {
remove();
listener.apply(this, args);
});
}
};
Or plain prototype with factory function...
const anEventEmitter = {
events: {},
on(event, listener) {
if (typeof this.events[event] !== 'object') {
this.events[event] = [];
}
this.events[event].push(listener);
return () => this.removeListener(event, listener);
},
removeListener(event, listener) {
if (typeof this.events[event] === 'object') {
const idx = this.events[event].indexOf(listener);
if (idx > -1) {
this.events[event].splice(idx, 1);
}
}
},
emit(event, ...args) {
if (typeof this.events[event] === 'object') {
this.events[event].forEach(listener => listener.apply(this, args));
}
},
once(event, listener) {
const remove = this.on(event, (...args) => {
remove();
listener.apply(this, args);
});
}
};
const makeEventEmitter = () => ({
__proto__: anEventEmitter,
events: {}
});
@sminutoli 's ES2015 classes implementation adapted to TypeScript, with a removeAllListeners()
function added:
type Listener = (...args: any[]) => void
type Events = { [event: string]: Listener[] };
export class MyEventEmitter {
private readonly events: Events = {};
constructor() {
}
public on(event: string, listener: Listener): () => void {
if(typeof this.events[event] !== 'object') this.events[event] = [];
this.events[event].push(listener);
return () => this.removeListener(event, listener);
}
public removeListener(event: string, listener: Listener): void {
if(typeof this.events[event] !== 'object') return;
const idx: number = this.events[event].indexOf(listener);
if(idx > -1) this.events[event].splice(idx, 1);
}
public removeAllListeners(): void {
Object.keys(this.events).forEach((event: string) =>
this.events[event].splice(0, this.events[event].length)
);
}
public emit(event: string, ...args: any[]): void {
if(typeof this.events[event] !== 'object') return;
this.events[event].forEach(listener => listener.apply(this, args));
}
public once(event: string, listener: Listener): void {
const remove: (() => void) = this.on(event, (...args: any[]) => {
remove();
listener.apply(this, args);
});
}
}
@shirakaba excellent code, a small improvement would be add a generic on the exported class like so
...
export class MyEventEmitter<T extends string> {
private readonly events: Events = {};
constructor() {
}
...
When you create a new instance you can pass the event strings
const emitter = new MyEventEmitter<"start" | "done">;
so you can have type check and suggestions
This issue is in the javascript version and the typescript version. The fix below is in typescript. There seems to be a problem with the once/removeListener function. When the event is emitted and the listener is called, it calls remove. The remove function splices the array stored at this.event[eventString] while the emit function is doing a forEach on the same array. This splice modifies the array while it is being iterated against, and causes the forEach to skip the next listener. A proposed solution to this would be:
public emit(event: string, ...args: any[]): void {
if (typeof this.events[event] !== "object") {
return;
}
[...this.events[event]].forEach((listener) => listener.apply(this, args));
}
The complete class:
type Listener = (...args: any[]) => void;
interface IEvents { [event: string]: Listener[]; }
export class EventEmitter {
private readonly events: IEvents = {};
public on(event: string, listener: Listener): () => void {
if (typeof this.events[event] !== "object") {
this.events[event] = [];
}
this.events[event].push(listener);
return () => this.removeListener(event, listener);
}
public removeListener(event: string, listener: Listener): void {
if (typeof this.events[event] !== "object") {
return;
}
const idx: number = this.events[event].indexOf(listener);
if (idx > -1) {
this.events[event].splice(idx, 1);
}
}
public removeAllListeners(): void {
Object.keys(this.events).forEach((event: string) =>
this.events[event].splice(0, this.events[event].length),
);
}
public emit(event: string, ...args: any[]): void {
if (typeof this.events[event] !== "object") {
return;
}
[...this.events[event]].forEach((listener) => listener.apply(this, args));
}
public once(event: string, listener: Listener): () => void {
const remove: (() => void) = this.on(event, (...args: any[]) => {
remove();
listener.apply(this, args);
});
return remove;
}
}
// Apply a ES6's new data structure Set
class EventEmitter{
constructor(){
this.events = {};
}
_getEventListByName(eventName){
if(typeof this.events[eventName] === 'undefined'){
this.events[eventName] = new Set();
}
return this.events[eventName]
}
on(eventName, fn){
this._getEventListByName(eventName).add(fn);
}
once(eventName, fn){
const self = this;
const onceFn = function(...args){
self.removeListener(eventName, onceFn);
fn.apply(self, args);
};
this.on(eventName, onceFn);
}
emit(eventName, ...args){
this._getEventListByName(eventName).forEach(function(fn){
fn.apply(this,args);
}.bind(this));
}
removeListener(eventName, fn){
this._getEventListByName(eventName).delete(fn);
}
}
Another TypeScript version but with wildcard support:
export class EventEmitter {
private events = {};
public on(event, listener) {
if (this.events[event] === undefined) {
this.events[event] = [];
}
this.events[event].push(listener);
return function() {
this.off(event, listener);
};
}
public off(event?, listener?) {
if (event === undefined && listener === undefined) {
this.events = {};
} else if (listener === undefined) {
delete this.events[event];
} else if (this.events[event].indexOf(listener) !== -1) {
this.events[event].splice(this.events[event].indexOf(listener), 1);
}
}
public emit(event, ...args) {
if (this.events[event] !== undefined) {
for (const listener of this.events[event]) {
listener(...args);
}
}
if (event !== "*") {
this.emit("*", ...args);
}
}
public once(event, listener) {
return this.on(event, () => {
this.emit(event);
this.off(event, listener);
});
}
}
enhanced with wildcard support for partial wildcard (for example, for events users.requested' and
users.loadedwe can subscribe to
users.or
users`)
type Listener = (...args: any[]) => void
interface IEvents {
[event: string]: Listener[]
}
export class EventEmitter {
private readonly events: IEvents = {}
private wildcardEvents: string[] = []
private recalcWildcardEvents() {
const newWildCardEvents = []
for (const i in this.events) {
if (i.endsWith('*') && this.events[i] && this.events[i].length > 0) {
newWildCardEvents.push(i)
}
}
this.wildcardEvents = newWildCardEvents
}
public on(event: string, listener: Listener): () => void {
if (typeof this.events[event] !== "object") {
this.events[event] = []
}
this.events[event].push(listener)
this.recalcWildcardEvents()
return () => this.removeListener(event, listener)
}
public removeListener(event: string, listener: Listener): void {
if (typeof this.events[event] !== "object") {
return
}
const idx: number = this.events[event].indexOf(listener)
if (idx > -1) {
this.events[event].splice(idx, 1)
}
this.recalcWildcardEvents()
}
public removeAllListeners(): void {
Object.keys(this.events).forEach((event: string) =>
this.events[event].splice(0, this.events[event].length),
)
this.recalcWildcardEvents()
}
public emit(event: string, ...args: any[]): void {
if (typeof this.events[event] === "object") {
[...this.events[event]].forEach((listener) => listener.apply(this, args))
}
if (event !== "*") {
this.emit("*", ...args)
}
for (const rawWcEvent of this.wildcardEvents) {
const wcEvent = rawWcEvent.slice(0, rawWcEvent.endsWith('.*') ? -2 : -1)
if (!event.endsWith('*') && event !== wcEvent && event.startsWith(wcEvent)) {
this.emit(rawWcEvent, event)
}
}
}
public once(event: string, listener: Listener): () => void {
const remove: (() => void) = this.on(event, (...args: any[]) => {
remove()
listener.apply(this, args)
this.recalcWildcardEvents()
})
return remove
}
}
Nice topic! A lot of interesting implementations
This is so awesome. Thank you all!
Spent the day working on this as it was an interview question on Pramp that really piqued my interest. Still don't fully understand the eventEmitter, bit it's a lot clearer now. Would be grateful for some feedback on my implementation. It's definitely a little verbose.
class EventEmitter {
constructor(event) {
this._events = {};
}
on = (event, listener) => {
if (typeof listener === "function") {
this._events[event] = [];
this._events[event].push(listener);
} else {
throw new Error(
" The listener argument must be of type Function. Received type undefined"
);
}
return this.eventEmitter;
};
// Adds a one time listener to the event. This listener is invoked only the next time the event is fired, after which it is removed.
once = (event, listener) => {
this._events[event].push({ listener: listener });
// Returns emitter, so calls can be chained.
return this.eventEmitter;
};
// Execute each of the listeners in order with the supplied arguments. Returns true if the event had listeners, false otherwise.
// emit
emit = (event, ...args) => {
for (let i = 0; i < this._events[event].length; i++) {
if (typeof this._events[event][i] === "function") {
this._events[event][i](args);
} else if (this._events[event][i] && this._events[event][i].listener) {
this._events[event][i].listener(...args);
delete this._events[event][i];
}
}
if (this._events[event].length) {
return true;
}
return false;
};
//Removes a listener from the listener array for the specified event. Caution − It changes the array indices in the listener array behind the listener. removeListener will remove, at most, one instance of a listener from the listener array. If any single listener has been added multiple times to the listener array for the specified event, then removeListener must be called multiple times to remove each instance. Returns emitter, so calls can be chained
off = (event, responseToEvent) => {
const eventArray = this._events[event];
let i = 0;
let deleteCount = 0;
if (typeof eventArray !== "undefined") {
while (deleteCount < 1) {
// console.log(eventArray[i] && typeof eventArray[i] === 'function');
if (typeof eventArray[i] === "function") {
eventArray.splice(i, 1);
deleteCount++;
}
i++;
}
}
return this.eventEmitter;
};
}
Use Symbol as event list key to avoid naming conflict:
const eventsKey = Symbol('events')
class EventEmitter {
constructor() {
this[eventsKey] = {}
}
// ...
}
You guys might want to check out EVT
The lib provides solution for things that can't easily be done with EventEmitter
like:
- Enforcing type safety.
- Removing a particular listener when the callback is an anonymous function.
- Adding a one-time listener for the next event that meets a condition.
- Waiting (via a Promise) for one thing or another to happen.
Example: waiting at most one second for the next message, stop waiting if the socket disconnects.
function EventEmitter() {
const eventRegister = {};
const on = (name, fn) => {
if (!eventRegister[name]) eventRegister[name] = [];
eventRegister[name].push(fn);
}
const trigger = (name) => {
if (!eventRegister[name]) return false;
eventRegister[name].forEach((fn) => fn.call());
}
const off = (name, fn) => {
if (eventRegister[name]) {
const index = eventRegister[name].indexOf(fn);
if (index >= 0) eventRegister[name].splice(index, 1);
}
}
return {
on, trigger, off
}
}
You might want to check this repo in order to find out how to create simple type safe event emitter library in Typescript.
Come from Pramp. Spent a lot of time figuring out this problem. There is an easier version without once implemented.
Two different ways to implement on and once, be aware of the difference.
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!(event in this.events)) {
this.events[event] = [];
}
this.events[event].push(listener);
return () => this.removeListener(event, listener);
}
removeListener(event, listener) {
if (!(event in this.events)) {
return;
}
const idx = this.events[event].indexOf(listener);
if (idx > -1) {
this.events[event].splice(idx, 1);
}
if (this.events[event].length === 0) {
delete this.events[event];
}
}
emit(event, ...args) {
if (!(event in this.events)) {
return;
}
this.events[event].forEach(listener => listener(...args));
}
once(event, listener) {
const remove = this.on(event, (...args) => {
remove();
listener(...args);
});
}
};
Second way:
on(event, listener) {
if (!(event in this.events)) {
this.events[event] = [];
}
this.events[event].push(listener);
// return () => this.removeListener(event, listener);
}
once(event, listener) {
const self = this;
this.on(event, function onceFn(...args) {
self.removeListener(event, onceFn);
listener(...args);
});
}
- Modified .on() to add the elements to the start of the array (slower).
- Modified .emit() to loop backwards through the events (much faster). This also avoids the race condition in the relationship between .once() and .emit() as noted by @undecidedapollo up there.
- Modified .removeAllListeners() to take an optional argument to allow the removal of all events associated with a specific name.
In a nutshell...
EventEmitter.prototype.on = function on( name, fn ) {
this.events[name] = [fn].concat( this.events[name] || [] );
}
EventEmitter.prototype.emit = function emit( name, data ) {
for ( let i = this.events[name].length - 1; i >= 0 ; --i ) {
this.events[name][i]( data );
}
}
EventEmitter.prototype.removeAllListeners = function removeAllListeners( name ) {
if ( name ) {
delete this.events[name];
} else {
// drop the old reference
this.events = {};
}
}
You can add simple overrides by returning true or false to stop propagation, e.g. return true to stop all other events before it.
// hooks.js
// inspired by gmod lua !
// it is useful to prevent circular dependencies and or import hell
export class hooks {
static register(name, f) {
if (!hooks[name])
hooks[name] = [];
hooks[name].push(f);
return f;
}
static unregister(name, f) {
hooks[name] = hooks[name].filter(e => e != f);
}
static call(name, x) {
if (!hooks[name])
return;
for (let i = hooks[name].length; i--;)
if (hooks[name][i](x))
return;
}
}
export default hooks;
Hello, this is actually amazing. What license do you release this under?
This was extracted from my Promise library Pacta and, as such, is released under the BSD 3-clause license.
@undecidedapollo and @coldsilk:
There seems to be a problem with the once/removeListener function. When the event is emitted and the listener is called, it calls remove. The remove function splices the array stored at this.event[eventString] while the emit function is doing a forEach on the same array. This splice modifies the array while it is being iterated against, and causes the forEach to skip the next listener.
It has been a while since I wrote this but I believe this is why emit
takes a shallow copy of the list of listeners on line 55 using slice
so that the loop is unaffected by removeListener
modifying the underlying events.
Thanks man! very helpful... I'm going to use it in my library if you don't mind :)