如何使用 JavaScript EventTarget?

IT技术 javascript events
2021-03-18 18:33:46

我想在我的客户端程序中创建一个自定义事件发射器。我正在参考EventTarget 的这个(稀疏)文档

我的实现尝试

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

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

我想要的用法

var e = new Emitter();

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

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

失败的地方

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

我究竟做错了什么?


更新

以下是可能的,但它是一个依赖于虚拟 DOMElement 的 hack

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

我想知道如何在不使用虚拟元素的情况下做到这一点

6个回答

我之前放弃了这个,但最近又需要它了。这是我最终使用的。

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

是的!


对于那些需要支持旧版本 ecmascript 的人,你去吧

// 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;
}

用法保持不变

如果您使用此解决方案,请记住this回调中的关键字指向 aDocumentFragment而不是您的对象,如果您需要避免此类行为,请使用bind回调方法,如e.addEventListener("something", callback.bind(e));
2021-05-06 18:33:46
@Scorpion,那么请分享解决此问题的最佳方法。我也没有看到“紧密耦合”,因为 Emitter 可以作为独立使用,也可以根据需要混合到其他对象中。对于 SO,我不会编写适用于所有浏览器的 polyfill,但Emitter它的好处在于它为您提供了一个修改需要跨浏览器兼容性的方法的好地方。
2021-05-08 18:33:46
@IvanCastellanos 是的,我想如果您依赖this它,则值得留意警告。
2021-05-08 18:33:46
@IvanCastellanos 不能在重定向实现中完成侦听器的绑定addEventListener吗?
2021-05-17 18:33:46
虽然您找到了解决方案,但这并不是解决此类问题的最佳方法。你在这里有一个紧耦合,这在某些浏览器中可能会失败。
2021-05-21 18:33:46

Bergi 关于这部分是正确的,那EventTarget只是一个接口而不是构造函数。

js 中有多个对象是有效的事件目标。正如那里提到的元素、文档和窗口是最常见的事件目标,但也有其他的,例如Websocket反正都是给的。

如果你做一个简短的测试,你会注意到一些事情:

EventTarget.isPrototypeOf(WebSocket); // true

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

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

typeof EventTarget // function

EventTarget() // TypeError: Illegal constructor

EventTarget是这些构造函数的原型,这是您无法为任何其他构造函数设置的(即使可以,它也可能无法工作)。它也是一个函数,但不是可调用的。

现在是你问的时候了:那么它有什么EventTarget好处以及我该如何使用它?

我们有每个事件发射器需要实现的 3 个方法,并且可能需要将这些方法绑定在一起,因此我们为它们提供了一个接口。这意味着您不能EventTarget用于调用目的,但其他一些本机函数可能会使用。这类似于创建元素,我们有document.createElement工厂方法,我们不(不能)new HTMLDivElement()用来创建新元素,但我们可以比较两个元素的构造函数。

结论

如果你想创建自定义事件发射器,你总是必须创建一些虚拟对象或使用一些已经存在的对象。在我看来,它是什么对象并不重要。

有些方法是不可调用的,但仍然可以作为对象的属性进行比较。因此它们是可见的。EventTarget是其中之一。

EventTarget现在在DOM 生活标准中被指定为可构造的它在支持Chrome浏览器64(已出)和火狐59(即将3月13日)。

快速检查 v8 显示e = Object.create(new EventTarget()); e.dispatchEvent(new Event('click'));产生“调用错误”,因此它仍然不可继承。
2021-04-21 18:33:46
@Kyle 能否请您澄清不可继承是什么意思 - 我在 idl 文件中看到大量基于 EventTarget 的定义
2021-04-26 18:33:46
@shabunc 我只是说,即使该方法存在,也无法调用dispatchEvent继承自的运行时对象EventTarget所有内置 DOM 元素等都EventTarget成功继承
2021-05-08 18:33:46

根据浏览器支持,有 3 种方法可以实现这一点。

1) EventTarget 现在是可构造的,所以只需扩展它:

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

2) DOM 'Node' 接口实现了 EventTarget,所以只需实现它:

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)推出自己的(假设没有选项arg)并调度异步:

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

如果需要,将 Map()/Set() 替换为 {}/[]。

所有这 3 个选项都可以通过以下方式进行测试:

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

任何需要实现您自己的“EventTarget”接口的对象都可以像原生接口一样继承它:

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

    get name(){ return this.__name;}
}
关于“自己动手”:dispatchEvent()必须同步调用侦听器,设置this、支持object.handleEvent和捕获异常。
2021-04-24 18:33:46

在不考虑浏览器支持的情况下EventTarget,无法将其实例化为构造函数,而仅使用另一个功能示例来丰富此问题。

根据Mozilla本身在该日期(2018 年 10 月 7 日)描述的兼容性列表

事件目标(构造函数):

  • 桌面
    • 铬 64
    • 火狐 59
    • 歌剧51
  • 手机
    • 网络视图 64
    • 铬安卓 64
    • 火狐安卓 59
    • 歌剧安卓 51

扩展:

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

你可以创建许多事件的插件常用的方法,如:on()off().once()emit()(使用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