Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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

This comment has been minimized.

Copy link

mauriciosoares commented Jun 15, 2014

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

@Xeoncross

This comment has been minimized.

Copy link

Xeoncross commented Jul 10, 2014

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

pmcalabrese commented Jun 22, 2018

@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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

AntonioArts commented Oct 24, 2019

Nice topic! A lot of interesting implementations

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.