Skip to content

Instantly share code, notes, and snippets.

@mudge
Last active December 4, 2024 08:35
Show Gist options
  • Save mudge/5830382 to your computer and use it in GitHub Desktop.
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);
});
};
@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