2. 事件驱动基础
说实话,很多同学刚开始接触事件驱动架构时,最困惑的就是这几个基础概念——事件、事件源、事件处理器、事件总线。它们听起来简单,但真正用起来,边界经常搞混。我当年第一次做事件系统时,就把事件源和事件处理器写到了一起,结果后面维护起来那叫一个痛苦。
今天咱们就把这四个概念彻底掰扯清楚。你理解透了,后面搭建架构时就不会走弯路。
2.1 事件(Event)—— 系统里的“消息快递”
事件是什么?说白了,就是“某件事已经发生了”这个事实的记录。它不是一个函数调用,也不是一个命令,而是一个已经发生的事实。
举个例子:用户点击了“提交订单”按钮,这不是事件——这只是操作。真正的事件是“订单已提交”,它包含了这个操作的结果数据。
事件的核心特征:
- 不可变性——事件一旦发生,就不能再修改。我见过有人试图在事件传递过程中修改事件数据,结果导致下游逻辑全乱套了。
- 自描述性——事件本身要包含足够的信息,让接收方能理解发生了什么。
- 时间戳——记录事件发生的精确时间,这对调试和审计特别重要。
来看一个典型的事件对象结构:
// 一个标准的事件对象
{
type: 'order:submitted', // 事件类型,用命名空间避免冲突
payload: { // 事件携带的数据
orderId: 'ORD-2024-001',
userId: 'U-12345',
totalAmount: 299.00,
items: ['item-1', 'item-2']
},
metadata: { // 元数据,描述事件本身
timestamp: 1704067200000, // 发生时间
source: 'checkout-page', // 来源标识
version: '1.0' // 事件版本,方便后续升级
}
}
我个人习惯在事件类型上使用domain:action的命名格式。比如order:submitted、payment:completed。这样一眼就能看出是哪个领域发生了什么动作。
小技巧:事件类型尽量用冒号分隔,而不是下划线或驼峰。因为很多事件总线工具(比如EventEmitter)支持用通配符匹配事件类型,冒号分隔的命名能让你更灵活地订阅一组相关事件。
2.2 事件源(Event Source)—— 谁发出了这个事件?
事件源就是事件的发起者。它可以是用户操作(点击、输入)、系统内部(定时器、状态变更)、或者外部系统(WebSocket消息、Webhook回调)。
你想想看,如果没有事件源的概念,你收到一个事件时根本不知道它从哪来的。调试的时候就像大海捞针。
事件源需要承担两个职责:
- 创建事件对象——把发生的事情转化为标准的事件结构
- 发布事件——把事件交给事件总线,而不是直接调用处理器
这里有个常见的坑:事件源不应该知道谁会处理这个事件。我曾经在一个项目里看到,事件源直接调用了处理器的回调函数,结果后面要加新的处理器时,还得改事件源的代码。这完全违背了事件驱动的解耦初衷。
// ❌ 错误做法:事件源直接调用处理器
class Button {
onClick() {
this.handler.handleClick(); // 紧耦合!
}
}
// ✅ 正确做法:事件源只负责发布事件
class Button {
onClick() {
const event = { type: 'ui:clicked', payload: { buttonId: this.id } };
EventBus.publish(event); // 不知道谁会处理,也不关心
}
}
2.3 事件处理器(Handler)—— 真正干活的角色
事件处理器就是监听并响应事件的函数或对象。它接收事件,然后执行相应的业务逻辑。
处理器有几个关键原则:
| 原则 | 说明 | 为什么重要 |
|---|---|---|
| 单一职责 | 一个处理器只处理一种事件 | 方便测试和维护,不会牵一发而动全身 |
| 无状态 | 处理器不保存状态,只依赖事件数据 | 可以随时替换、重启,不会丢失上下文 |
| 幂等性 | 同一个事件处理多次,结果一样 | 防止事件重复投递导致数据不一致 |
| 快速返回 | 处理器应该尽快完成,不要阻塞 | 避免拖慢事件总线的吞吐量 |
注意:处理器里不要做耗时操作,比如发HTTP请求、读写大文件。如果必须做,请把任务丢到队列里异步执行。我见过一个线上事故,就是因为处理器里同步调用了外部API,结果API超时,整个事件循环卡死了。
来看一个标准的处理器实现:
// 订单提交事件的处理器
const orderSubmittedHandler = {
// 声明要处理的事件类型
eventType: 'order:submitted',
// 处理逻辑
handle(event) {
const { orderId, userId, totalAmount } = event.payload;
// 1. 验证事件数据
if (!orderId || !userId) {
console.error('订单数据不完整', event);
return;
}
// 2. 执行业务逻辑
this.sendConfirmationEmail(userId, orderId);
this.updateInventory(event.payload.items);
this.recordAnalytics(totalAmount);
// 3. 可以发布新的事件
EventBus.publish({
type: 'order:confirmed',
payload: { orderId, confirmedAt: Date.now() }
});
},
sendConfirmationEmail(userId, orderId) {
// 发送邮件逻辑
},
updateInventory(items) {
// 更新库存逻辑
},
recordAnalytics(amount) {
// 记录分析数据
}
};
2.4 事件总线(Event Bus)—— 架构的“中枢神经”
事件总线是连接事件源和处理器的桥梁。它不关心事件的具体内容,只负责两件事:接收事件和分发事件。
你可以把事件总线想象成一个邮局。事件源是寄信人,处理器是收信人。邮局不关心信里写了什么,只负责把信送到正确的人手里。
事件总线的核心能力:
- 发布(publish)——接收事件源发来的事件
- 订阅(subscribe)——让处理器注册自己感兴趣的事件类型
- 取消订阅(unsubscribe)——让处理器取消注册
- 事件路由——根据事件类型找到对应的处理器
下面我用SVG画一张事件驱动架构的核心流程图,帮你直观理解这四个概念的关系:
2.5 四个概念如何协同工作?
理解了每个概念后,咱们看看它们在实际中是怎么配合的。我拿一个常见的“用户注册”场景来演示:
- 事件源:注册页面表单提交按钮被点击,创建一个
user:registered事件 - 事件总线:收到事件,查找所有订阅了
user:registered的处理器 - 处理器A:发送欢迎邮件
- 处理器B:初始化用户默认配置
- 处理器C:记录注册来源的统计分析
你看,注册页面(事件源)完全不需要知道后面有三个处理器在干活。哪天想加个“发送短信通知”的处理器,直接注册到事件总线上就行,注册页面的代码一行都不用改。
核心要点总结:
- 事件是数据,不是行为
- 事件源只负责发布,不负责处理
- 处理器只负责响应,不关心来源
- 事件总线是中介,实现完全解耦