什么时候应该使用基于事件的编程?


65

我一直在传递回调或只是从程序中的其他函数触发函数,以使任务完成后就可以进行操作。完成后,我将直接触发该函数:

var ground = 'clean';

function shovelSnow(){
    console.log("Cleaning Snow");
    ground = 'clean';
}

function makeItSnow(){
    console.log("It's snowing");
    ground = 'snowy';
    shovelSnow();
}

但是我已经阅读了许多编程方面的策略,据我了解它强大但尚未实践的策略是基于事件的(我认为我所了解的一种方法称为“ pub-sub”):

var ground = 'clean';

function shovelSnow(){
    console.log("Cleaning Snow");
    ground = 'clean';
}

function makeItSnow(){
    console.log("It's snowing");
    ground = 'snowy';
    $(document).trigger('snow');
}

$(document).bind('snow', shovelSnow);

我想了解基于事件的编程的客观优缺点,而不是仅仅从其他函数中调用所有函数。在什么情况下可以使用基于事件的编程?


2
顺便说一句,您可以使用$(document).bind('snow', shovelShow)。无需将其包装在匿名函数中。
Karl Bielefeldt 2014年

4
您可能还对学习“反应式编程”感兴趣,这与事件驱动的编程有很多共同之处。
埃里克·利珀特

Answers:


75

事件是描述最近发生的事件的通知。

事件驱动系统的典型实现利用事件分派器处理程序功能(或订户)。调度程序提供一个API来将处理程序连接到事件(jQuery的bind),以及一种将事件发布给其订阅者的方法(trigger在jQuery中)。在谈论IO或UI事件时,通常还会有一个事件循环,该循环检测新事件,例如单击鼠标,并将其传递给调度程序。在JS-land中,调度程序和事件循环由浏览器提供。

对于直接与用户交互的代码(响应按键和单击),事件驱动的编程(或其变体,例如功能性反应式编程)几乎是不可避免的。您(程序员)不知道用户何时何地单击,因此取决于GUI框架或浏览器来检测其事件循环中用户的操作并通知您的代码。这种类型的基础结构也用在网络应用程序中(参见NodeJS)。

在您的示例中,您在代码中引发一个事件而不是直接调用一个函数,这有一些更有趣的折衷,我将在下面讨论。主要区别在于,事件(makeItSnow)的发布者未指定呼叫的接收者;它连接到其他地方(bind在您的示例的调用中)。这就是所谓的“一劳永逸”makeItSnow向世界宣布正在下雪,但它并不关心谁在听,接下来会发生什么或何时发生-它只是在广播消息并从手中抹去灰尘。


因此,事件驱动的方法使消息的发送方与接收方分离。这给您带来的一个好处是,一个给定的事件可能具有多个处理程序。您可以将一个gritRoads函数绑定到您的snow事件,而不会影响现有的shovelSnow处理程序。您可以灵活地连接应用程序;要关闭行为,您只需要删除该bind调用,而不是遍历代码以查找行为的所有实例。

事件驱动编程的另一个优点是,它为您提供了解决跨领域问题的空间。事件分发程序扮演Mediator的角色,某些库(例如Brighter)利用管道,因此您可以轻松地插入通用要求,例如日志记录或服务质量。

完全公开:Brighter是我在Huddle工作的地方开发的。

去耦从接收事件的发送方的第三个优点是,它为您提供了灵活性,你处理该事件。您可以在自己的线程上处理每种类型的事件(如果您的事件分配器支持的话),也可以将引发的事件放置在诸如RabbitMQ之类的消息代理上,并通过异步过程进行处理,甚至整夜处理它们。事件的接收者可以处于单独的进程中,也可以位于单独的机器上。您不必更改引发事件的代码即可执行此操作!这是“微服务”架构背后的宏伟构想:自治服务使用事件进行通信,并将消息中间件作为应用程序的主干。

对于事件驱动样式的另一个完全不同的示例,请查看域驱动设计,其中域事件用于帮助使聚合保持独立。例如,考虑一家在线商店,该商店根据您的购买历史推荐产品。一个Customer需要当有它的购买历史记录更新ShoppingCart的支付。该ShoppingCart骨料可通知Customer通过引发CheckoutCompleted事件; 该Customer会在单独的事务响应事件得到更新。


这种事件驱动模型的主要缺点是间接的。现在很难找到处理事件的代码,因为您不能仅使用IDE导航到该事件;您必须弄清楚该事件在配置中的绑定位置,并希望您已找到所有处理程序。随时都有更多东西要记住。代码样式约定可以为您提供帮助(例如,将所有调用都bind放在一个文件中)。为了您的理智,仅使用一个事件分发程序并始终使用它很重要。

另一个缺点是难以重构事件。如果需要更改事件的格式,则还需要更改所有接收者。当事件的订阅者位于不同的计算机上时,这会加剧,因为现在您需要同步软件版本!

在某些情况下,性能可能是一个问题。处理消息时,调度程序必须:

  1. 在某些数据结构中查找正确的处理程序。
  2. 为每个处理程序建立一条消息处理管道。这可能涉及大量内存分配。
  3. 动态调用处理程序(如果语言需要,可以使用反射)。

这肯定比常规的函数调用慢,后者仅涉及将新帧压入堆栈。但是,事件驱动的体系结构为您提供的灵活性使隔离和优化慢速代码变得更加容易。能够将工作提交给异步处理器是这​​里的一大胜利,因为它使您可以在后台处理辛苦工作的同时立即处理请求。无论如何,如果您正在与数据库交互或在屏幕上绘图,那么IO的成本将完全淹没处理消息的成本。这是避免过早优化的情况。


总而言之,事件是构建松耦合软件的好方法,但并非没有代价。例如,用事件替换应用程序中的每个函数调用将是一个错误。使用事件进行有意义的架构划分。


2
这个答案与我选择的5377答案相同。我正在更改选择以标记该名称,因为它进一步详细说明。
Viziionary 2014年

1
速度是否是事件驱动代码的主要缺点?似乎可以,但是我不太清楚。
raptortech97

1
@ raptortech97当然可以。对于需要特别快速的代码,您可能希望避免在内部循环中发送事件;幸运的是,在这种情况下,它通常可以很好地定义您需要执行的操作,因此您不需要特别灵活的事件(或发布/订阅或观察者,这是具有不同术语的等效机制)。
Jules

1
还要注意,围绕角色模型构建了一些语言(例如,Erlang),其中一切都是消息(事件)。在这种情况下,编译器可以决定将消息/事件实现为直接函数调用还是通信实现。
布伦丹2015年

1
对于“性能”,我认为我们需要区分单线程性能和可伸缩性。消息/事件对于单线程性能可能会更糟(但可以将其转换为函数调用,而零附加成本也不会更糟),并且对于可伸缩性,消息/事件在几乎所有方面都具有优越性(例如,可能导致现代多线程系统的性能大幅提高) -CPU和将来的“许多CPU”系统)。
布伦丹2015年

25

当程序不控制其执行的事件顺序时,将使用基于事件的编程。相反,程序流由外部进程(例如用户(例如GUI),另一个系统(例如客户端/服务器)或另一个进程(例如RPC))控制。

例如,批处理脚本知道它需要做什么,因此它就可以做到。它不是基于事件的。

文字处理器坐在那里,等待用户开始打字。按键是触发功能以更新内部文档缓冲区的事件。该程序不知道您要键入什么,因此它必须是事件驱动的。

大多数GUI程序都是事件驱动的,因为它们是围绕用户交互构建的。但是,基于事件的程序并不限于GUI,这只是大多数人最熟悉的示例。Web服务器等待客户端连接并遵循类似的习惯用法。您计算机上的后台进程也可能会响应事件。例如,按需病毒扫描程序可以从OS接收有关新创建或更新的文件的事件,然后在该文件中扫描病毒。


18

在基于事件的应用程序中,事件侦听器的概念将使您能够编写更多松散耦合的应用程序。

例如,第三方模块或插件可以从数据库中删除一条记录,然后触发该receordDeleted事件,并将其余的留给事件侦听器来完成其工作。即使触发模块甚至不知道谁在监听此特定事件或接下来将发生什么,一切都可以正常工作。


6

我想补充一个简单的类比对我有帮助:

将您的应用程序的组件(或对象)视为一大批Facebook朋友。

当您的一个朋友想告诉您一些事情时,他们可以直接给您打电话或将其发布到他们的Facebook墙上。当他们将其发布到他们的Facebook上时,任何人都 可以看到它并对其做出反应,但是很多人却没有。有时候,人们可能需要对此做出反应很重要,例如“我们正在生孩子!” 或“某某乐队正在Drunkin'Clam酒吧做一场惊喜音乐会!”。在最后一种情况下,其他朋友可能需要对此做出反应,特别是如果他们对该乐队感兴趣的话。

如果您的朋友想在您和他们之间保守秘密,他们可能不会将其发布到他们的Facebook墙上,他们会直接给您打电话并告诉您。设想一个场景,您告诉一个您喜欢的女孩,您想在餐馆见她约会。您可以直接将其发布到Facebook墙上,以供所有朋友查看,而不必直接打电话给她并问她。这行得通,但是如果您有嫉妒的前任,那么她可以看到并出现在餐厅破坏您的一天。

在决定是否构建事件监听器以实现某些东西时,请考虑一下这种类比。此组件是否需要将业务放在那里供任何人查看?还是他们需要直接打电话给某人?事情很容易变得混乱,所以要小心。


0

下面的类比可以通过在医生接待台画一条平行于等待线的方式来帮助您了解事件驱动的I / O编程。

阻止I / O就像,如果您站在队列中,接待员请您前面的一个人填写表格,然后她等待直到完成为止。您必须等到轮到他完成表格为止,这很阻塞。

如果单身男人需要3分钟填写,则第十个人必须等到30分钟。现在要减少这10个人的等待时间,解决方案将是增加接待员的数量,这是昂贵的。这就是传统Web服务器中发生的情况。如果您请求用户信息,则其他用户的后续请求应等待,直到从数据库获取的当前操作完成为止。这增加了第十个请求的“响应时间”,并且对第n个用户呈指数增长。为了避免这种传统的Web服务器,为每个单个请求创建线程(相当于增加接收者的数量),也就是说,基本上,它为每个请求创建服务器的副本,这是CPU消耗的昂贵代价,因为每个请求都需要操作系统线。为了扩大应用规模,

事件驱动:增加队列的“响应时间”的另一种方法是采用事件驱动的方法,其中队列中的人将被交出表格,要求填写并返回完成。因此,接待员可以随时提出要求。从一开始,这就是javascript一直在做的事情。在浏览器中,javascript将响应用户单击事件,滚动,滑动或数据库获取等。本质上,这在javascript中是可能的,因为javascript将函数视为第一类对象,并且它们可以作为参数传递给其他函数(称为回调),并且可以在完成特定任务时调用。这正是node.js在服务器上所做的事情。你可以找到关于事件驱动编程和阻塞I / O的详细信息,在节点的情况下在这里

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.