How to use JavaScript EventTarget?

前端 未结 10 1766
一生所求
一生所求 2020-12-01 03:53

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

My implementation atte

相关标签:
10条回答
  • 2020-12-01 04:02

    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 

    0 讨论(0)
  • 2020-12-01 04:10

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

    0 讨论(0)
  • 2020-12-01 04:15

    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

    0 讨论(0)
  • 2020-12-01 04:16

    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(); 
    
    0 讨论(0)
  • 2020-12-01 04:22

    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'))

    0 讨论(0)
  • 2020-12-01 04:23

    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!"
    
    
    0 讨论(0)
提交回复
热议问题