Skip to content

Instantly share code, notes, and snippets.

@mudge
Last active April 29, 2024 07:48
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);
});
};
@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