41

I would like to create a custom event emitter in my client-side programs. I am referencing this (sparse) documentation for EventTarget

My implementation attempt

var Emitter = function Emitter() {
  EventTarget.call(this);
};

Emitter.prototype = Object.create(EventTarget.prototype, {
  constructor: {
    value: Emitter
  }
});

My desired usage

var e = new Emitter();

e.addEventListener("hello", function() {
  console.log("hello there!");
});

e.dispatchEvent(new Event("hello"));
// "hello there!"

Where it fails

var e = new Emitter();
// TypeError: Illegal constructor

What am I doing wrong?


Update

The following is possible, but it's a hack that depends on a dummy DOMElement

var fake = document.createElement("phony");
fake.addEventListener("hello", function() { console.log("hello there!"); });
fake.dispatchEvent(new Event("hello"));
// "hello there!"

I'd like to know how to do this without having to use the dummy element

Mulan
  • 129,518
  • 31
  • 228
  • 259

10 Answers10

45

I gave up on this awhile ago, but recently needed it again. Here's what I ended up using.

ES6

class Emitter {
  constructor() {
    var delegate = document.createDocumentFragment();
    [
      'addEventListener',
      'dispatchEvent',
      'removeEventListener'
    ].forEach(f =>
      this[f] = (...xs) => delegate[f](...xs)
    )
  }
}

// sample class to use Emitter
class Example extends Emitter {}

// run it
var e = new Example()
e.addEventListener('something', event => console.log(event))
e.dispatchEvent(new Event('something'))

ES5

function Emitter() {
  var eventTarget = document.createDocumentFragment()

  function delegate(method) {
    this[method] = eventTarget[method].bind(eventTarget)
  }

  [
    "addEventListener",
    "dispatchEvent",
    "removeEventListener"
  ].forEach(delegate, this)
}

// sample class to use it
function Example() {
  Emitter.call(this)
}

// run it
var e = new Example()

e.addEventListener("something", function(event) {
  console.log(event)
})

e.dispatchEvent(new Event("something"))

Yeah!


For those that need to support older versions of ecmascript, here you go

// IE < 9 compatible
function Emitter() {
  var eventTarget = document.createDocumentFragment();

  function addEventListener(type, listener, useCapture, wantsUntrusted) {
    return eventTarget.addEventListener(type, listener, useCapture, wantsUntrusted);
  }

  function dispatchEvent(event) {
    return eventTarget.dispatchEvent(event);
  }

  function removeEventListener(type, listener, useCapture) {
    return eventTarget.removeEventListener(type, listener, useCapture);
  }

  this.addEventListener = addEventListener;
  this.dispatchEvent = dispatchEvent;
  this.removeEventListener = removeEventListener;
}

The usage stays the same

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    While you found a solution, it isn't the best way to solve problems like this one. You have a tight-coupling here and that could fail in some browsers. – Yang Jun 14 '14 at 04:46
  • 1
    @Scorpion, then please share the best way to solve this problem. I also see no "tight coupling" as Emitter can be used as a standalone or mixed into your other objects as desired. For SO, I'm not going to write a polyfill that works in every browser, but the nice part about `Emitter` is it gives you a good place to modify methods that need cross-browser compatibility. – Mulan Jun 14 '14 at 05:06
  • 2
    If you use this solution keep in mind that keyword `this` inside the callback points to a `DocumentFragment` instead of your object, if you need to avoid such behavior use the `bind` method for the callback, as in `e.addEventListener("something", callback.bind(e));` – Ivan Castellanos Jul 10 '16 at 23:45
  • @IvanCastellanos Yeah, I suppose if you're relying on `this` it's worth heeding the warning. – Mulan Jul 14 '16 at 23:33
  • @IvanCastellanos Couldn't that binding of the listener also be done in the redirection implementation of `addEventListener`? – ygoe Nov 30 '18 at 21:36
  • wonder if we could make it work with typescript – Valen Apr 22 '22 at 14:20
9

Bergi was right about the part, that EventTarget is just an interface and not a constructor.

There are multiple objects in js that are valid event targets. As mentioned there: Element, document, and window are the most common event targets, but there are also others for example Websocket. Anyway, all of them are given.

If you make a short test, you can notice few things:

EventTarget.isPrototypeOf(WebSocket); // true

var div = document.createElement("div");

EventTarget.isPrototypeOf(div.constructor); // true

typeof EventTarget // function

EventTarget() // TypeError: Illegal constructor

EventTarget is prototype of these constructors, which is something you can't set for any other constructor (and even if you could, it wouldnt probably work). Also it is a function, but not callable one.

Now this is the time when you ask: So what is it EventTarget good for and how can I use it?

We have 3 methods that each event emitter needs to implement and there was probably a need to bind these methods together, so we have an interface for them. Which means you can't use EventTarget for calling purposes, but some other native functions might. This is similar like creating elements, we have document.createElement factory method and we don't (can't) use new HTMLDivElement() to create a new element, but we can compare constructors of two elements.

Conclusion

If you want to create custom event emitter, you always have to create some dummy object or use some that already exists. From my point of view, it doesn't matter what object it will be.

Some methods are not callable, but still can be compared as properties of objects. Therefore they are visible. EventTarget is one of them.

Entity Black
  • 3,401
  • 2
  • 23
  • 38
7

There are 3 ways to achieve this depending on browser support.

1) EventTarget is now constructable, so just extend it:

class MyEventTarget extends EventTarget {
    constructor(){
        super()
    }
}

2) The DOM 'Node' interface implements EventTarget, so just implement that instead:

function MyEventTarget(){
    var target = document.createTextNode(null);
    this.addEventListener = target.addEventListener.bind(target);
    this.removeEventListener = target.removeEventListener.bind(target);
    this.dispatchEvent = target.dispatchEvent.bind(target);
}
MyEventTarget.prototype = EventTarget.prototype;

3) Roll your own (assuming no options arg) & dispatch async:

function MyEventTarget(){
    this.__events = new Map();
}
MyEventTarget.prototype = {
    addEventListener(type, listener){
        var listeners = this.__events.get(type);
        if(!listeners){
            listeners = new Set();
            this.__events.set(type, listeners);
        }
        listeners.add(listener);
    },

    removeEventListener(type, listener){
        var listeners = this.__events.get(type);
        if(listeners){
            listeners.delete(listener);
            if(listeners.size === 0){
                this.__events.delete(type);
            }
        }
    },

    dispatchEvent(event){
        var listeners = this.__events.get(event.type);
        if(listeners){
            for(let listener of listeners){
                setTimeout(listener.call(null, event), 0);
            }
        }
    }
}

Replace Map()/Set() with {}/[] if required.

All 3 of these options can be tested with:

var target = new MyEventTarget();
target.addEventListener('test', (e) => {console.log(e.detail);}, false);

var event = new CustomEvent('test', {detail : 'My Test Event'});
target.dispatchEvent(event);

Any object that needs to implement your own 'EventTarget' interface can inherit it exactly as the native one does:

function Person(name){
    MyEventTarget.call(this);
    this.__name = name;
}
Person.prototype = {
    __proto__ : MyEventTarget.prototype,

    get name(){ return this.__name;}
}
Neil
  • 1,928
  • 19
  • 14
  • 1
    About "roll your own": `dispatchEvent()` must call listeners synchronously, set `this`, support `object.handleEvent`, and catch exceptions. – CoolCmd Apr 11 '19 at 08:33
6

EventTarget is now specified as constructible in the DOM living standard. It is supported in Chrome 64 (already out) and Firefox 59 (coming March 13).

guest
  • 6,450
  • 30
  • 44
  • 2
    quick check in v8 shows that `e = Object.create(new EventTarget()); e.dispatchEvent(new Event('click'));` yields an "invocation error" so it is still not inheritable. – Kyle Oct 24 '18 at 19:48
  • @Kyle can you please clarify what do you mean by non-inheritable - I see tons of EventTarget-based definitions in idl files – shabunc Dec 07 '18 at 12:03
  • 1
    @shabunc i simply mean that it's not possible to call `dispatchEvent` on a runtime object that inherits from `EventTarget` even though the method exists. all the builtin DOM elements and such do inherit from `EventTarget` successfully. – Kyle Jan 28 '19 at 20:41
5

Without taking into consideration browser support where EventTarget can not be instantiated as a constructor and only to enrich this issue with yet another functional example.

According to the compatibility list described by Mozilla itself in this date (October 7, 2018):

EventTarget (constructor):

  • desktop:
    • Chrome 64
    • Firefox 59
    • Opera 51
  • mobile:
    • WebView 64
    • Chrome Android 64
    • Firefox Android 59
    • Opera Android 51

Extends:

class Emitter extends EventTarget {
    constructor() {
        super()
    }
}

You could create common methods in many event plugins like: on(), off(), .once() and emit() (using CustomEvent):

/**
 * Emmiter - Event Emitter
 * @license The MIT License (MIT)             - [https://github.com/subversivo58/Emitter/blob/master/LICENSE]
 * @copyright Copyright (c) 2020 Lauro Moraes - [https://github.com/subversivo58]
 * @version 0.1.0 [development stage]         - [https://github.com/subversivo58/Emitter/blob/master/VERSIONING.md]
 */
const sticky = Symbol()
class Emitter extends EventTarget {
    constructor() {
        super()
        // store listeners (by callback)
        this.listeners = {
            '*': [] // pre alocate for all (wildcard)
        }
        // l = listener, c = callback, e = event
        this[sticky] = (l, c, e) => {
            // dispatch for same "callback" listed (k)
            l in this.listeners ? this.listeners[l].forEach(k => k === c ? k(e.detail) : null) : null
        }
    }
    on(e, cb, once = false) {
        // store one-by-one registered listeners
        !this.listeners[e] ? this.listeners[e] = [cb] : this.listeners[e].push(cb);
        // check `.once()` ... callback `CustomEvent`
        once ? this.addEventListener(e, this[sticky].bind(this, e, cb), { once: true }) : this.addEventListener(e, this[sticky].bind(this, e, cb))
    }
    off(e, Fn = false) {
        if ( this.listeners[e] ) {
            // remove listener (include ".once()")
            let removeListener = target => {
                this.removeEventListener(e, target)
            }
            // use `.filter()` to remove expecific event(s) associated to this callback
            const filter = () => {
                this.listeners[e] = this.listeners[e].filter(val => val === Fn ? removeListener(val) : val);
                // check number of listeners for this target ... remove target if empty
                this.listeners[e].length === 0 ? e !== '*' ? delete this.listeners[e] : null : null
            }
            // use `while()` to iterate all listeners for this target
            const iterate = () => {
                let len = this.listeners[e].length;
                while (len--) {
                    removeListener(this.listeners[e][len])
                }
                // remove all listeners references (callbacks) for this target (by target object)
                e !== '*' ? delete this.listeners[e] : this.listeners[e] = []
            }
            Fn && typeof Fn === 'function' ? filter() : iterate()
        }
    }
    emit(e, d) {
        this.listeners['*'].length > 0 ? this.dispatchEvent(new CustomEvent('*', {detail: d})) : null;
        this.dispatchEvent(new CustomEvent(e, {detail: d}))
    }
    once(e, cb) {
        this.on(e, cb, true)
    }
}

const MyEmitter = new Emitter()

// one or more listeners for same target ...
MyEmitter.on('xyz', data => {
    console.log('first listener: ', data)
})
MyEmitter.on('xyz', data => {
    console.log('second listener: ', data)
})

// fire event for this target
MyEmitter.emit('xyz', 'zzzzzzzzzz...') // see listeners show

// stop all listeners for this target
MyEmitter.off('xyz')

// try new "emit" listener event ?
MyEmitter.emit('xyz', 'bu bu bu') // nothing ;)

// fire a "once" ? Yes, fire
MyEmitter.once('abc', data => {
    console.log('fired by "once": ', data)
})

// run
MyEmitter.emit('abc', 'Hello World') // its show listener only once

// test "once" again
MyEmitter.emit('abc', 'Hello World') // nothing 
Lauro Moraes
  • 1,358
  • 2
  • 14
  • 16
3

EventType() constructor is now supported in most modern browsers.

For the browsers which still do not support it, there is a polyfill available.

This means that it's as simple as:

var e = new EventTarget();

e.addEventListener("hello", function() {
  console.log("hello there!");
});

e.dispatchEvent(new CustomEvent("hello"));
// "hello there!"

For Internet Explorer, which doesn't support CustomEvent being used this way, there is code for a polyfill listen on the MDN page or a package on GitHub and npm

For the sake of completeness, in Node or an Electron app you would do

var EventEmitter = require('events');

var e = new EventEmitter();

e.addListener("hello", function() {
  console.log("hello there!");
});

e.emit("hello")
// "hello there!"

Codebling
  • 10,764
  • 2
  • 38
  • 66
2

Here is how to do it using CustomEvent, cross-browser (fiddle):

// listen to event
window.addEventListener("say", function(e) { alert(e.detail.word); });

// create and dispatch the event
var event = document.createEvent("CustomEvent");
event.initCustomEvent('say', true, true, 
    { "word": "Hello!" });

window.dispatchEvent(event);

You'd need to use window or document or any other existing DOM element to register listeneres and dispatch the event. EventTarget is not a object, it's an interface. Try accessing EventTarget in JavaScript console and you'll see that.

noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    MDN documentation specifically says this usage is ["old-fashioned"](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events#The_old-fashioned_way) and it kind of sucks that I have to attach to the window (global object). Anyway, I've made an edit to show how you can improve this. Still, it would be nice to encapsulate the events. – Mulan Mar 05 '14 at 02:11
  • @naomik, the edit you made doesn't work in IE9, IE10, IE11. I rolled it back. – noseratio Mar 05 '14 at 04:43
  • I think it would've been more constructive to make a note for that solution then. Now we just have a solution using the old Java-influenced API that doesn't even fully solve my problem. And for what it's worth, [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) is only documented to support IE >= 9.0, so it's a safe assumption that I'm not looking for an answer that supports an earlier version of IE. – Mulan Mar 05 '14 at 05:34
  • @naomik, it's not about `EventTarget`, it's about `new CustomEvent(...)` **not working in IE9-IE11**, a sad fact. So you have to stick with `document.createEvent("CustomEvent")`, unless you don't care about IE at all. That's why I rolled back your edit. – noseratio Mar 05 '14 at 06:00
  • As to `EventTarget`, it doesn't exist in IE9-11 either, but that's expected. In the DOM standard, [it's defined as *interface*](http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget) on a DOM Node object, rather than a standalone object. Although Firefox and Chrome have chosen to implement it as an object, Microsoft may never follow this, as it is a non-standard approach. – noseratio Mar 05 '14 at 06:27
0

Try my simple ES6 implemetation.

class DOMEventTarget {
  constructor() {
    this.listeners = new Map();
  }
  addEventListener(type, listener) {
    this.listeners.set(listener.bind(this), {
      type, listener
    });
  }
  removeEventListener(type, listener) {
    for(let [key, value] of this.listeners){
      if(value.type !== type || listener !== value.listener){
        continue;
      }
      this.listeners.delete(key);
    }
  }
  dispatchEvent(event) {
    Object.defineProperty(event, 'target',{value: this});
    this['on' + event.type] && this['on' + event.type](event);
    for (let [key, value] of this.listeners) {
      if (value.type !== event.type) {
        continue;
      }
      key(event);
    }
  }
}

let eventEmitter = new DOMEventTarget();
eventEmitter.addEventListener('test', e => {
  console.log('addEventListener works');
});
eventEmitter.ontest = e => console.log('ontype works');
eventEmitter.dispatchEvent(new Event('test'));
Lewis
  • 14,132
  • 12
  • 66
  • 87
-1

There are two ways to implement the EventTarget "Interface".

1) Like mdn suggests use javascript prototypes. In my opinion this is clearly not the best approach to do this. The simple reason is that everybody who does use your library has to know that he needs to add a listeners property to his constructor function.

function implement_event_target_interface(target_constructor_function) 
{
    target_constructor_function.prototype.listeners = null;
    target_constructor_function.prototype.addEventListener = function(type, callback) {
        if (!(type in this.listeners)) {
            this.listeners[type] = [];
        }
        this.listeners[type].push(callback);
    };

    target_constructor_function.prototype.removeEventListener = function(type, callback) {
        if (!(type in this.listeners)) {
            return;
        }
        var stack = this.listeners[type];
        for (var i = 0, l = stack.length; i < l; i++) {
            if (stack[i] === callback){
            stack.splice(i, 1);
            return;
            }
        }
    };

    target_constructor_function.prototype.dispatchEvent = function(event) {
        if (!(event.type in this.listeners)) {
            return true;
        }
        var stack = this.listeners[event.type].slice();

        for (var i = 0, l = stack.length; i < l; i++) {
            stack[i].call(this, event);
        }
        return !event.defaultPrevented;
    };
}

let Person = function()
{
    this.listeners = {}; // Every contructor that implements the event_target_interface must have this property. This is not very practical and intuitive for the library-user.

    this.send_event = function() {
        var event = new CustomEvent('test_event', { 'detail': "test_detail" });
        this.dispatchEvent(event);
    }
}

implement_event_target_interface(Person);

let person = new Person();

person.addEventListener('test_event', function (e) { 
    console.log("catched test_event from person")
}.bind(this), false);

person.send_event();

And not only that, it gets even worse when you use constructor inheritance on Person, because you also need to inherit the prototype in order to be able to send events.

let Student = function() {
    Person.call(this);
}

Student.prototype = Person.prototype;
Student.prototype.constructor = Student;

let student = new Student();

student.addEventListener('test_event', function (e) { 
    console.log("catched test_event from student")
}.bind(this), false);

student.send_event();

2) Use constructor inheritance. Much much better.

function EventTarget() 
{
    this.listeners = {};

    this.addEventListener = function(type, callback) {
        if (!(type in this.listeners)) {
            this.listeners[type] = [];
        }
        this.listeners[type].push(callback);
    };

    this.removeEventListener = function(type, callback) {
        if (!(type in this.listeners)) {
            return;
        }
        var stack = this.listeners[type];
        for (var i = 0, l = stack.length; i < l; i++) {
            if (stack[i] === callback){
            stack.splice(i, 1);
            return;
            }
        }
    };

    this.dispatchEvent = function(event) {
        if (!(event.type in this.listeners)) {
            return true;
        }
        var stack = this.listeners[event.type].slice();

        for (var i = 0, l = stack.length; i < l; i++) {
            stack[i].call(this, event);
        }
        return !event.defaultPrevented;
    };
}

let Person = function()
{
    EventTarget.call(this);

    this.send_event = function() {
        var event = new CustomEvent('test_event', { 'detail': "test_detail" });
        this.dispatchEvent(event);
    }
}

let person = new Person();

person.addEventListener('test_event', function (e) { 
    console.log("catched test_event from person")
}.bind(this), false);

person.send_event(); 
Ini
  • 548
  • 7
  • 19
  • Invader this is fundamentally incorrect, please see my answer – Neil Aug 23 '18 at 09:52
  • Can you please explain what is incorrect? I did not say that these are the only two ways. Yes you can also use the new EventTarget constructor. Defenitely not worth a downvote, since my answer at least points out pros and cons instead of just giving three examples and not pointing out anything about why and when to use or not use an approach x. – Ini Aug 23 '18 at 14:03
  • 1) The EventTarget is a constructor and has been for a while so is both directly construtcable and inheritable. 2) Your comment about the difficulties of inheriting an implementation (which I hope I addressed) and 3) the main reason - your assertion that 'constructor inheritance is much much better'. It very rarely is anyway, but here it is especially bad as your implementation is not in the prototype chain, which makes it significantly less efficient and consumes more memory. Remember EventTarget is the base interface of every single DOM Node object. That was the reason for the down vote. – Neil Aug 23 '18 at 16:56
  • You assume that the prototype chain inheritance performance gains outweight their friction, which is debatable. Using classes over functions has some pros and cons. Furthermore Javascript is clearly not a language that you pick for performance. Of course when you gain something you always loose something too. When you think your approach is the best in every aspect, I have to say "Your approach is fundamentelly incorrect". – Ini Aug 23 '18 at 18:02
  • I believe that I'm 100% correct that the MDN approach is much worse then the contructor inheritance solution that I proposed even though it's less performant. – Ini Aug 23 '18 at 18:04
  • I don't assume it is more performant - I categorically know it is. Aside that my custom implementation didn't use a class it used native prototype inheritance, either will be orders of magnitude more efficient than yours. You offered poor, inaccurate advice, and I've called you on it. You should have wondered _why_ MDN used prototypical inheritance in their example. Feel free to test in Chrome Dev tools to satisfy yourself. – Neil Aug 23 '18 at 18:06
  • So in your opinion if more performance, it is the better soluton regardless of all other properties? This is defenitely not correct. I could give you examples, but we go out of scope. – Ini Aug 23 '18 at 18:18
  • You could have stated "your second solution has less performance" and not that it is "fundamentally wrong" – Ini Aug 23 '18 at 18:18
  • Btw why is it in your opinion both less less efficient and more memory consuming? I can be wrong but these two seem to contradict with eachother in javascript prototype chain theory (spec). Furthermore in my tests show absolute insignicant differences. – Ini Aug 23 '18 at 18:46
  • Maybe the ES6 way is more how it's expected to be done in JS, but both ways have it's merits in terms of memory and speed, there is no one best way. – Ini Sep 04 '18 at 21:28
-2

sample code snippet to use javascript EventTarget

// attach event var ev = EventTarget.prototype.addEventListener.call(null, 'alert', () => alert('ALERTED')) // dispatch event ev.dispatchEvent.call(null, new Event('alert'))

Imamudin Naseem
  • 1,604
  • 18
  • 21
  • how can you scope these events tho? are they all just global then? – Mulan Dec 14 '16 at 14:20
  • @user633183, all callers, which specify `null`, will share one "object" (scope) `null`. And I think this **HACK** will stop working when browsers implement native private properties. – CoolCmd Apr 11 '19 at 09:03