如何使用JavaScript EventTarget?

时间:2014-03-05 01:03:21

标签: javascript events

我想在客户端程序中创建自定义事件发射器。我正在引用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

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

我想知道如何在不使用虚拟元素的情况下执行此操作

10 个答案:

答案 0 :(得分:31)

我前一段时间放弃了这一点,但最近再次需要它。这就是我最终使用的内容。

  

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

用法保持不变

答案 1 :(得分:8)

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

js中有多个对象是有效的事件目标。如上所述there元素,文档和窗口是最常见的事件目标,但也有其他例如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就是其中之一。

答案 2 :(得分:6)

EventTarget现在在DOM living standard中被指定为可构造的。它由Chrome 64(已经出局)和Firefox 59(即将于3月13日)支持。

答案 3 :(得分:2)

有3种方法可以实现此目的,具体取决于浏览器的支持情况。

1)EventTarget现在是可构造的,因此只需对其进行扩展即可:

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

2)DOM“节点”接口实现了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()。

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

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

答案 4 :(得分:1)

以下是使用CustomEvent,跨浏览器(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);

您需要使用windowdocument或任何其他现有DOM元素来注册侦听并分派事件。 EventTarget不是对象,它是一个接口。尝试在JavaScript控制台中访问EventTarget,您会看到。

答案 5 :(得分:1)

在不考虑浏览器支持的情况下,EventTarget不能实例化为构造函数,而只是通过另一个功能示例来丰富此问题。

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

EventTarget (构造函数):

  • 桌面
    • Chrome 64
    • Firefox 59
    • 歌剧51
  • 移动
    • WebView 64
    • Chrome Android 64
    • Firefox Android 59
    • Opera Android 51

扩展:

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

您可以在许多事件插件中创建通用方法,例如:on()off().once()emit()(使用CustomEvent):

// copyright, license and more examples see: https://github.com/subversivo58/Emitter
class Emitter extends EventTarget {
    constructor() {
        super()
        // store listeners
        this.listeners = {}
    }
    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, cb, { once: true }) : this.addEventListener(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 => {
                    return val === Fn ? removeListener(val) : val
                })
                // check number of listeners for this target ... remove target if empty
                if ( this.listeners[e].length === 0 ) {
                    delete this.listeners[e]
                }
            }    

            // use `.forEach()` to iterate all listeners for this target
            const iterate = () => {
                this.listeners[e].forEach((val, index, array) => {
                    removeListener(val)
                    // on end "loop" remove all listeners reference for this target (by target object)
                    if ( index === array.length -1 ) {
                        delete this.listeners[e]
                    }
                })
            }    

            Fn && typeof Fn === 'function' ? filter() : iterate()
        }
    }
    emit(e, d) {
        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 => {
    // yep, date is a `CustomEvent` object so use the "detail" property for get data
    console.log('first listener: ', data.detail)
})
MyEmitter.on('xyz', data => {
    // yep, date is a `CustomEvent` object so use the "detail" property for get data
    console.log('second listener: ', data.detail)
})

// 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.detail)
})

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

// test "once" again
MyEmitter.emit('abc', 'Hello World') // nothing

请注意,return是CustomEvent对象,因此,要实际获取数据,必须使用“详细信息”属性:

MyEmitter.on('target-event', data => {
    console.log(data.detail)
})

答案 6 :(得分:1)

{ "message": "The given data was invalid.", "errors": { "name": [ "The name field is required." ], "email": [ "The email has already been taken." ] } } 的构造函数现在为supported in most modern browsers

对于仍不支持它的浏览器,有polyfill可用。

这意味着它很简单:

const express = require('express');
const exphbs = require('express-handlebars');
const app = express();

app.engine('handlebars', exphbs({ defaultLayout: 'main' }));
app.set('view engine', 'handlebars');

app.use(express.static('public'));

app.get('/', (req, res) => {
  res.render('home', { title: 'Home Page' });
});

app.get('/about', (req, res) => {
  res.render('about', { title: 'About Us Page' });
});

app.listen(3000, () => console.log('server run!'));

对于不支持以这种方式使用EventType()的Internet Explorer,有code for a polyfill listen on the MDN page或软件包on GitHubnpm

为了完整起见,在Node或Electron应用程序中,您应该这样做

var e = new EventTarget();

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

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

答案 7 :(得分:0)

尝试我的简单ES6实现。

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

答案 8 :(得分:-1)

有两种方法可以实现EventTarget“接口”。

1)像mdn suggests一样使用 javascript原型。我认为这显然不是最好的方法。原因很简单,每个使用您的库的人都必须知道他需要在其构造函数中添加一个listeners属性。

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

不仅如此,当您在Person上使用构造方法继承时,情况甚至更糟,因为您还需要继承原型才能发送事件。

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)使用构造函数继承好得多

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

答案 9 :(得分:-2)

使用javascript EventTarget的示例代码段

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