事件是描述最近发生的事件的通知。
事件驱动系统的典型实现利用事件分派器和处理程序功能(或订户)。调度程序提供一个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
放在一个文件中)。为了您的理智,仅使用一个事件分发程序并始终使用它很重要。
另一个缺点是难以重构事件。如果需要更改事件的格式,则还需要更改所有接收者。当事件的订阅者位于不同的计算机上时,这会加剧,因为现在您需要同步软件版本!
在某些情况下,性能可能是一个问题。处理消息时,调度程序必须:
- 在某些数据结构中查找正确的处理程序。
- 为每个处理程序建立一条消息处理管道。这可能涉及大量内存分配。
- 动态调用处理程序(如果语言需要,可以使用反射)。
这肯定比常规的函数调用慢,后者仅涉及将新帧压入堆栈。但是,事件驱动的体系结构为您提供的灵活性使隔离和优化慢速代码变得更加容易。能够将工作提交给异步处理器是这里的一大胜利,因为它使您可以在后台处理辛苦工作的同时立即处理请求。无论如何,如果您正在与数据库交互或在屏幕上绘图,那么IO的成本将完全淹没处理消息的成本。这是避免过早优化的情况。
总而言之,事件是构建松耦合软件的好方法,但并非没有代价。例如,用事件替换应用程序中的每个函数调用将是一个错误。使用事件进行有意义的架构划分。
$(document).bind('snow', shovelShow)
。无需将其包装在匿名函数中。