在自定义对象上添加事件监听器

68
我已经创建了一个对象,该对象有几个方法。其中一些方法是异步的,因此我想使用事件来在方法完成时执行操作。为此,我尝试将 addEventListener 添加到对象中。

jsfiddle

var iSubmit = {
    addEventListener: document.addEventListener || document.attachEvent,
    dispatchEvent: document.dispatchEvent,
    fireEvent: document.fireEvent,   


    //the method below is added for completeness, but is not causing the problem.

    test: function(memo) {
        var name = "test";
        var event;
        if (document.createEvent) {
            event = document.createEvent("HTMLEvents");
            event.initEvent(name, true, true);
        } else {
            event = document.createEventObject();
            event.eventType = name;
        }
        event.eventName = name;
        event.memo = memo || { };

        if (document.createEvent) {
            try {
                document.dispatchEvent(event);
            } catch (ex) {
                iAlert.debug(ex, 'iPushError');
            }
        } else {
            document.fireEvent("on" + event.eventType, event);
        }
    }
}

iSubmit.addEventListener("test", function(e) { console.log(e); }, false);


//This call is added to have a complete test. The errors are already triggered with the line before this one.

iSubmit.test();

这将返回一个错误:添加事件监听器失败:TypeError:在不实现EventTarget接口的对象上调用 'addEventListener'

现在,这段代码将被用于一个PhoneGap应用程序中,在Android / iOS上可以工作。然而,在测试期间,如果我至少能让它在一个浏览器中工作,那就太好了。

PS> 我知道我可以启用冒泡,然后监听文档根,但我想有一点面向对象编程,使每个对象都能独立工作。


Nodejs有这个功能。他们做了类似于var emitter = require('events').EventEmitter;的事情。但是我们能在普通的JS中像这样做吗?ourObject.prototype.__proto__ = EventTarget.prototype; - Siva Tumma
可能是在自己的对象中实现事件的重复问题。 - rogerdpack
15个回答

55

addEventListener 用于实现某些事件相关接口的 DOM 元素。如果您想在纯 JavaScript 对象上使用事件系统,则应寻找自定义事件系统。例如,在 Backbone.js 中,Backbone.Events 就是一个例子。基本思想是使用对象作为哈希来跟踪已注册的回调函数。

个人建议使用:emitter

它是一个相当简单而优雅的解决方案-具有像 on(), off()emit() 这样甜美且简短的方法名。您可以通过 new Emitter() 创建新实例,或者通过 Emitter(obj) 在现有对象中混合事件功能。请注意,此库是为使用 CommonJS 模块系统编写的,但是您可以通过删除 module.exports = ... 行在任何其他地方使用它。


8
谢谢。我想答案是“无法完成”。正如您已经提到的,您的插件只是一个回调系统。我宁愿重写我的代码来使用回调,而不是使用第三方插件。 - Hugo Delsing
5
事件必须触发回调,因此如果你愿意这样称呼,事件系统本质上就是一个回调系统。EventEmitter模式在Node.js和大型前端应用程序中非常常见,因此使用一个成熟的现有解决方案并不是个坏主意。 - Evan You
使用本地方法的优点在于通常情况下本地代码(例如HTMLElement.addEventListener)在性能上优于用javascript编写的javascript代码。 - Tomáš Zato
1
在我的情况下,我设置了一个单一的代理文档片段,然后使用标识符来关联片段和对象(类似于 '__id'=4 的扩展属性)。在对象上分派事件实际上会发生在片段上,这将触发处理程序适当地设置事件详细信息和目标。所有这些都发生在包装数组对象(如jQuery)中,因此很简单。不确定它是否比直接回调、承诺或其他方法更快——DOM通常非常复杂和缓慢——但它运行良好,足够快,并且易于实现。 - jimmont
@SandeepDixit,它是:new EventTarget,事件使用dispatchEvent分派 - 这是完全可用的。这个答案只是来自6年前,因为它可以在各种浏览器上可靠地工作。 - Benjamin Gruenbaum
显示剩余2条评论

17

如果你不需要真实的事件特性(比如冒泡、stopPropagation),那么你可以实现自己的事件。addEventListener只是DOM的一个API,所以在DOM之外的对象中,你并不真正需要它。如果你想在一个对象周围创建一个事件模式,这里有一种很好的方法可以做到这一点,它不需要任何额外的浏览器API,并且应该非常向后兼容。

假设你有一个对象,当调用dispatch方法时,你希望触发一系列事件:


var OurDispatcher, dispatcher;

OurDispatcher = (function() {
  function OurDispatcher() {
    this.dispatchHandlers = [];
  }

  OurDispatcher.prototype.on = function(eventName, handler) {
    switch (eventName) {
      case "dispatch":
        return this.dispatchHandlers.push(handler);
      case "somethingElse":
        return alert('write something for this event :)');
    }
  };

  OurDispatcher.prototype.dispatch = function() {
    var handler, i, len, ref;
    ref = this.dispatchHandlers;
    for (i = 0, len = ref.length; i < len; i++) {
      handler = ref[i];
      setTimeout(handler, 0);
    }
  };

  return OurDispatcher;

})();

dispatcher = new OurDispatcher();

dispatcher.on("dispatch", function() {
  return document.body.innerHTML += "DISPATCHED</br>";
});

dispatcher.on("dispatch", function() {
  return document.body.innerHTML += "DISPATCHED AGAIN</br>";
});

dispatcher.dispatch();

总的来说,它并不需要更加复杂。采用这种方法可以在事件上获得良好控制,而且不需要担心向后兼容性或外部库,因为所有内容都得到广泛支持。从技术上讲,您甚至可以不使用 setTimeout 并处理没有任何 API 的回调。任何其他类似 stopPropagation() 的东西都必须自己处理。

https://jsfiddle.net/ozsywxer/

当然,CustomEvent 有一些 polyfills,但除非我需要高级事件功能,否则我更喜欢将自己的事件系统封装成一个“class”,并将其他类/函数与其扩展。

以下是 CoffeeScript 版本,JavaScript 是由此派生的: https://jsfiddle.net/vmkkbbxq/1/

^^ 更易理解一些。


17

如果您想监听JavaScript对象,有三种方式:

关于sub/pub模式:

您需要发布事件。

关于原生实现:

  • 使用Object get/set operators足以监听添加、删除、更改和获取事件。这些操作符有良好的支持,只在IE8-中存在问题。但如果要在IE8中使用get/set,请在DOM对象上使用Object.defineProperty或使用sham
  • Object.prototype.watch有良好的ES5 polyfill
  • Proxy API需要ES Harmony支持。

Object.observe示例

var o = {};
Object.observe(o, function (changes) {
  changes.forEach(function (change) {
    // change.object contains changed object version
    console.log('property:', change.name, 'type:', change.type);
  });
});
o.x = 1     // property: x type: add
o.x = 2     // property: x type: update
delete o.x  // property: x type: delete

很遗憾,对于我当前的问题,没有什么可以用的,因为有更简单的解决方案来完成我尝试做的简单任务。但是你确实向我展示了一些新的东西,我会在另一个项目中尝试。谢谢! - Hugo Delsing
我修改了我的回答,因为有Object.observe可以像你所需的那样工作。让我们留在这里给未来的访问者和用户。 - Alex
1
Object.observe()现已被弃用。我已编辑您的帖子,以警告其他人不要使用它。希望这个更新有所帮助!https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe - andersfylling

10

仅是猜测;我自己没有尝试过。但是,您可以创建一个虚拟元素,并在虚拟元素上触发/监听事件。 此外,我更喜欢不使用库。

function myObject(){
    //create "dummy" element
    var dummy = document.createElement('dummy');
    //method for listening for events
    this.on = function(event, func){dummy.addEventListener(event, func);};
    //you need a way to fire events
    this.fireEvent = function(event, obj){
      dummy.dispatchEvent(new CustomEvent(event, {detail: obj}));
    }
}
//now you can use the methods in the object constructor
var obj = new myObject();
obj.on("custom", function(e){console.log(e.detail.result)});
obj.fireEvent("custom", {result: "hello world!!!"});

嘿!我正在使用它来制作我的原型,希望它能够进入生产阶段。附言:我们有一些共同的想法。 - rvsingh42

10

有两个问题。

首先,iSubmit.addEventListener() 方法实际上是 EventTarget DOM 接口上的一个方法:

这些仅用于 DOM 元素。通过将其添加到 iSubmit 对象作为方法,您正在对不是 EventTarget 的对象调用它。这就是为什么 Chrome 抛出一个 Uncaught TypeError: Illegal invocation JavaScript 错误。

第一个问题非常重要,但如果您可以使用 EventTarget#addEventListener(),因为事件是添加到 iSubmit 但从 document 处理,所以您的代码将无法工作。通常,在附加事件侦听器和处理事件时需要使用相同对象的方法(除非您使用冒泡事件,这是一个不同的故事 - 注意:冒泡不限于 JavaScript 或 DOM 相关事件,例如)。

在您自己的对象中使用自定义事件非常正常。正如Evan Yu提到的那样,有关此事的库。以下是几个:

我使用了 js-signals 并且相当喜欢它。我从未使用过 Wolfy87/EventEmitter,但它看起来很不错。

如果您使用了 js-signals,您的示例可能如下所示:

jsFiddle

var iSubmit = {
    finished: new signals.Signal(),
    test: function test(memo) {
        this.finished.dispatch(memo || {});
    }
};

iSubmit.finished.add(function(data) {
    console.log('finished:', data);
});

iSubmit.test('this is the finished data');


// alternatively
iSubmit.finished.dispatch('this is dispatched directly from the signal');

谢谢。所以它无法完成...太糟糕了。但我宁愿使用自己的回调函数,而不是使用插件来代替我完成它。 - Hugo Delsing
没问题,很高兴能帮到你。 - tiffon

8

这是一个简单的事件触发器:

class EventEmitter {
    on(name, callback) {
        var callbacks = this[name];
        if (!callbacks) this[name] = [callback];
        else callbacks.push(callback);
    }

    dispatch(name, event) {
        var callbacks = this[name];
        if (callbacks) callbacks.forEach(callback => callback(event));
    }
}

用法:

var emitter = new EventEmitter();

emitter.on('test', event => {
    console.log(event);
});

emitter.dispatch('test', 'hello world');

5

如果您在Node.js环境中,则可以使用Node的EventEmitter类

CustomObject.js

const EventEmitter = require('events');

class CustomObject extends EventEmitter {
  constructor() {
    super();
  }

  doSomething() {
    const event = {message: 'Hello World!'};
    this.emit('myEventName', event);
  }
}

module.exports = CustomObject;

使用方法:

const CustomObject = require('./CustomObject');

// 1. Create a new instance
const myObject = new CustomObject();

// 2. Subscribe to events with ID "myEventName"
myObject.on('myEventName', function(event) {
  console.log('Received event', event);
});

// 3. Trigger the event emitter
myObject.doSomething();

如果你想在Node.js环境之外使用Node的EventEmitter,那么你可以使用webpack(最好是v2.2或更高版本)来获取一个捆绑了你的CustomClass和一个由webpack构建的EventEmitter polyfill的包。
以下是它的工作方式(假设你使用npm install -g webpack全局安装了webpack):
  1. 运行webpack CustomObject.js bundle.js --output-library=CustomObject
  2. 在你的HTML页面中包含bundle.js(它将暴露window.CustomObject
  3. 没有第三步!

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
    <script src="bundle.js"></script>
  </head>
  <body>
    <script>
      // 1. Create a new instance
      const myObject = new window.CustomObject();

      // 2. Subscribe to events with ID "myEventName"
      myObject.on('myEventName', function(event) {
        console.log('Received event', event);
      });

      // 3. Trigger the event emitter
      myObject.doSomething();
    </script>
  </body>
</html>

2
你不知道分享这个对我有多大帮助!!!谢谢! - ryanwaite28

4
我可以通过将一个元素包装成Javascript类来实现这个目标。 重要的是,该元素不必存在于DOM中。此外,该元素的标签名称可以是任何自定义类名。
class MyClass
{   
    
    constructor(options ) 
    {     
    
  
        this.el =  document.createElement("MyClass");//dummy element to manage events.    
        this.el.obj= this; //So that it is accessible via event.target.obj
      
    }

    addEventListener()
    {
 
        this.el.addEventListener(arguments[0],arguments[1]);

    }
    
 

     raiseEvent()
         {
//call this function or write code below when the event needs to be raised.

            var event = new Event('dataFound');
            event.data = messageData;
            this.el.dispatchEvent(event);
        }
}


let obj = new MyClass();
obj.addEventListener('dataFound',onDataFound);

function onDataFound()
{ 
console.log('onDataFound Handler called');
}

'''


1
使用createElement创建一个虚拟元素。
typescript
class Person {
    name: string
    el: HTMLElement // event listener
    constructor(name: string) {
        this.name = name
        this.el = document.createElement("Person"); // dummy element to manage events
        (this.el as any).object = this // set dummy attribute. (Optional) So that it is accessible via `event.target.object`
    }

    AddEventListener(type: string, listener: any) {
        this.el.addEventListener(type, listener)
    }

    DispatchEvent(type: string, data: any = null) {
        const event = new Event(type);
        (event as any).data = data //dummy attribute (Optional)
        this.el.dispatchEvent(event)
    }
}

const carson = new Person("Carson")
carson.AddEventListener("Say", (e: Event) => {
    const person = (e.target as any).object as Person // get dummy attribute
    const data = (e as any).data // get dummy attribute
    if (data !== undefined && data.stopImmediatePropagation === true) {
        e.stopImmediatePropagation()
    }
    console.log(`${person.name}`, data)
})
carson.AddEventListener("Say", () => {
    console.log("Say2")
})

carson.DispatchEvent("Say")
// Output:
// Carson undefined
// Say2
carson.DispatchEvent("Say", "hello world!")
// Carson hello world!
// Say2
carson.DispatchEvent("Say", {stopImmediatePropagation: true})
// Carson {stopImmediatePropagation: true}

可运行的示例

<script>
  class Person {
    constructor(name) {
      this.name = name
      this.el = document.createElement("Person") // dummy element to manage events
      this.el.object = this // set dummy attribute. (Optional) So that it is accessible via `event.target.object`
    }

    AddEventListener(type, listener) {
      this.el.addEventListener(type, listener)
    }

    DispatchEvent(type, data) {
      const event = new Event(type)
      event.data = data // set dummy attribute
      this.el.dispatchEvent(event)
    }
  }

  const carson = new Person("Carson")
  carson.AddEventListener("Say", (e) => {
    const person = e.target.object // get dummy attribute
    const data = e.data // get dummy attribute
    if (data !== undefined && data.stopImmediatePropagation === true) {
      e.stopImmediatePropagation()
    }
    console.log(`${person.name}`, data)
  })

  carson.AddEventListener("Say", (e) => {
    console.log("Say2")
  })

  carson.DispatchEvent("Say")
  carson.DispatchEvent("Say", "hello world!")
  carson.DispatchEvent("Say", {stopImmediatePropagation: true})
</script>


1
这篇文章介绍了如何创建自定义事件:http://www.sitepoint.com/javascript-custom-events/
以下是一个例子:
创建事件 -
var event = new CustomEvent(
    "newMessage",
    {
        detail: {
            message: "Hello World!",
            time: new Date(),
        },
        bubbles: true,
        cancelable: true
    }
);

将事件分配给某个东西 -
document.getElementById("msgbox").dispatchEvent(event);

订阅事件 -
document.addEventListener("newMessage", newMessageHandler, false);

谢谢回复。不幸的是,创建事件并不是问题所在。问题在于将监听器方法添加到对象上。我不想在“document”上监听,而是在“iSubmit”上监听。 - Hugo Delsing
我明白了。这个线程上的两个答案可能会解决您的问题:https://dev59.com/EVHTa4cB1Zd3GeqPOQ0a - Dawson Loudon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接