Skip to content

Instantly share code, notes, and snippets.

@mudge
Last active February 26, 2024 09:42
Star You must be signed in to star a gist
Save mudge/5830382 to your computer and use it in GitHub Desktop.
A very simple EventEmitter in pure JavaScript (suitable for both node.js and browsers).
/* 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);
});
};
@mauriciosoares
Copy link

Thanks man! very helpful... I'm going to use it in my library if you don't mind :)

@xeoncross
Copy link

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.

@mykeels
Copy link

mykeels commented Dec 11, 2017

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)

@Raphael909
Copy link

Raphael909 commented Dec 21, 2017

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);
        });
    };
};

@sminutoli
Copy link

sminutoli commented Jun 18, 2018

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: {}
});

@shirakaba
Copy link

shirakaba commented Jun 18, 2018

@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);
        });
    }
}

@pmcalabrese
Copy link

@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

@undecidedapollo
Copy link

undecidedapollo commented Jul 25, 2018

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;
    }
}

@ianva
Copy link

ianva commented Aug 29, 2018

// 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);
  }


}

@brianjenkins94
Copy link

brianjenkins94 commented Aug 10, 2019

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);
		});
	}
}

@fend25
Copy link

fend25 commented Oct 6, 2019

enhanced with wildcard support for partial wildcard (for example, for events users.requested' and users.loadedwe can subscribe tousers.orusers`)

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
  }
}

@AntonioArts
Copy link

Nice topic! A lot of interesting implementations

@laurengarcia
Copy link

This is so awesome. Thank you all!

@feargswalsh92
Copy link

feargswalsh92 commented Jan 17, 2020

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;
  };
}

@alphakevin
Copy link

Use Symbol as event list key to avoid naming conflict:

const eventsKey = Symbol('events')

class EventEmitter {
  constructor() {
    this[eventsKey] = {}
  }
  // ...
}

@garronej
Copy link

garronej commented Feb 19, 2020

You guys might want to check out EVT

image

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.

@jonathanbsilva
Copy link

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
  }
}

@infinitum11
Copy link

You might want to check this repo in order to find out how to create simple type safe event emitter library in Typescript.

@uop789
Copy link

uop789 commented Jul 22, 2021

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);  
  });
}

@coldsilk
Copy link

  • 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 = {};
    }
}

@otse
Copy link

otse commented Feb 9, 2023

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;

@aggregate1166877
Copy link

Hello, this is actually amazing. What license do you release this under?

@mudge
Copy link
Author

mudge commented Nov 20, 2023

Hi @aggregate1166877,

This was extracted from my Promise library Pacta and, as such, is released under the BSD 3-clause license.

@mudge
Copy link
Author

mudge commented Nov 20, 2023

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment