JavaScript中PubSub/过多事件和事件处理程序的性能成本是多少?

26
"

发布/订阅 / 事件驱动架构 是客户端和服务器端JavaScript世界中的常见做法。我被委托使用Dojo作为前端和node.js作为后端来设计一个非常大的Web应用程序。发布/订阅似乎非常吸引人,因为它允许团队之间进行大量并行处理。但我担心会有性能后果。

"
我是一名有用的助手,可以翻译文本。
我有一个关于 JavaScript 中事件和事件处理程序成本的一般性问题。我已经看到了 this, this, this, 甚至 thisthis。但我似乎仍然没有看到一个通用的答案。不考虑框架,假设我们有2个方法
publish() //Like jQuery's / Dojo's trigger(), EventEmitter's emit()

subscribe() //Like jQuery's / Dojo's / EventEmiter's / DOM's on() connect() live() addEventListener()

问题1:每个事件触发器的成本是多少?

案例1:强调发布/订阅的更干净(松散耦合)代码

object.publish('message1', data);
object.publish('message2', data);
...
object.publish('message100', data);

//All these are in separate files / modules    
subscribe (object, 'message1', function (data) { A()...})
subscribe (object, 'message2', function (data) { B()...})
subscribe (object, 'message100', function (data) { Z()...})

案例二:代码耦合度高!但它是否更具性能?

data.message = 'message1'
object.publish('message', data)
subscribe (object, 'message', function (data) {
  switch (data) {
    case 'message1':
      A();
      break();    
    case 'message2':
      B();
      break();
     ...
    case 'message100':
      Z();
      break();
  }
})

问题2:每个事件监听器的成本是多少?

object.publish('event', data);

案例1:再次强调Pub/Sub的干净(松散耦合)代码

//A.js    
subscribe (object, 'event', function (data) {
   A();
});

//B.js
subscribe (object, 'event', function (data) {
   B();
});

//C.js
subscribe (object, 'event', function (data) {
   C();
});

案例2:再次,紧密耦合的代码!但它是否更具性能?

subscribe (object, 'event', function (data) {
   A();
   B();
   C();
});

问题1:有人能指导我在客户端(使用DOMEvents或Custom Events)、服务器端(EventEmitter等)进行的相关研究和性能测试吗?这只是一个简单的例子,但应用程序很快就可以扩展到数千个这样的调用。如果没有,如何测试自己是否存在明显的性能下降?也许可以使用jsperf之类的工具?是否有任何理论基础可以了解为什么其中一个更加高效?

问题2:如果Case 1更高效,那么编写松散耦合代码的最佳方法是什么?有没有找到折中方案的方法?像 Case 1 一样编写代码,但使用一些中间编译/构建过程将其转换为 Case 2 (类似于Google Closure编译器在其他性能情况下所做的)。例如使用[Esprima]。我不想让构建过程变得更加复杂。如果有任何性能提升,这是否值得?

问题3:最后,虽然我在这里寻求非常具体的JavaScript答案,但了解其他语言/环境中的性能成本可能会有所帮助。事实上,在大多数情况下,事件都是由硬件触发的(使用中断的概念),这对答案有什么贡献吗?

感谢所有坚持到这个问题结尾的人!!!非常感谢!!!


你可以编写自己的测试并亲自查看,http://jsperf.com/ - nicholaswmin
1
@NicholasKyriakides,重点不仅在于编写测试(尽管我已经提到我需要帮助),还在于理论基础,即为什么一种方法比另一种更有效,并找到在性能和代码清晰度之间取得平衡的最佳方法。 - Gaurav Ramanan
这可能会随着每次重新实现而改变。你需要测试,因为一个在V8(chrome/node.js)中可能更高效,而另一个在FF/IE/任何其他浏览器中表现更好。我不确定在这里是否可能将理论和实践分开。我怀疑对于你的特定情况,就像大多数情况一样,JavaScript性能不管怎样都不会成为瓶颈,发布/订阅模式很可能更容易构建/维护。 - Jared Smith
@JaredSmith 我认为事件是JS中非常基础的基础,我真的觉得它是一个核心语言部分,实现可能并不像我们想象的那么不同。嗯,至少在V8中表现是一致的。 - Gaurav Ramanan
1
你可能是对的,但就性能而言,你几乎总是更好地关注以下几点:CSS动画、http缓存头、图像优化、不必要的ajax、阻塞、布局触发变化、绘制触发变化等等。所有这些东西在浏览器中对性能的影响都比js重要得多。对于node来说,它几乎肯定会在服务器上运行,如果由于某种原因它不够快,你很可能(相比开发时间)可以便宜地投入硬件。与维护噩梦相比较。 - Jared Smith
1
我并没有看到这两种方式有很大的区别。在某个地方,你会根据一个字符串进行切换。无论是在 EventEmitter 中还是在接收事件后,都会有一些东西在事件名称上进行切换。真正的诀窍是消除对字符串的切换 - 回调是一种方法,但不是另一个答案使用它们的方式。理论上,我认为基于数字的消息系统会更快。实现 on(messageId: Number, ...) 而不是 on(messageType: String, ...)。然后,您可以使用 messageId 作为回调数组中的偏移量:callbacks[messageId] - David Griffin
1个回答

12

正如你所提到的,每个架构决策都会产生性能影响:


回调函数(最快)

一对一的绑定直接引用函数

315,873 ops/sec

发布-订阅事件

太多绑定需要循环调用多个回调函数,回调函数越多=性能越差

291,609 ops/sec

承诺(最慢)

使用延迟确保链中的每个项目不会同时被调用,链条越多=性能越差

253,301 ops/sec


大多数情况下我会选择回调函数,除非你有很多相互连接的模块,那么发布-订阅就很有用。

请记住,它们可以相互配合。我通常在同一模块中允许回调函数和发布-订阅事件,让开发人员选择他们更喜欢使用的那个:

var module = {
    events: [],
    item: { 'name': 'Test module' },
    updateName: function(value, callback) {
        this.item.name = value;
        if (callback) {
            callback(this.item);
        }
        this.dispatchEvent('update', this.item);
    },
    addEvent: function(name, callback) {
        if (!this.events[name]) { this.events[name] = []; }
        this.events[name].push(callback);
    },
    removeEvent: function(name, callback) {
        if (this.events[name]) {
            if (callback) {
                for (var i=0; i<this.events[name].length; ++i) {
                  if (this.events[name][i] === callback) { this.events[name].splice(i, 1); return true; }
                }
            }
            else { delete this.events[name]; }
        }
    },
    dispatchEvent: function(name, data) {
        if (this.events[name]) {
            for (var i=0; i<this.events[name].length; ++i) {
                this.events[name][i]({ data: data, target: this, type: name});
            }
        }
    }
};

module.updateName('Dan'); // no callback, no event
module.updateName('Ted', function (item) { console.log(item); }); // callback, no event
module.addEvent('update', function (item) { console.log(item); }); // add an event
module.updateName('Sal'); // no callback, event callback
module.addEvent('update', function (item) { console.log('event2', item); }); // add a second event
module.updateName('Jim'); // no callback, two event callbacks

1
感谢您的回答。它为应用程序结构与回调提供了新的维度。然而,我的基本问题仍然是关于性能的。如果能够得到一些理论证明支持的基准数据就更好了。 - Gaurav Ramanan
1
我在js perf上创建了一个示例,展示了前两种方法:http://jsperf.com/callback-vs-pub-sub 在大多数浏览器中,回调的速度是发布订阅的两倍,并且随着添加更多回调,发布订阅的性能会降低。 - Kim T
1
在此链接中添加了 Promise 的示例:https://jsperf.com/callback-vs-pubsub-vs-promise - Kim T

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