# 使用 Async Generator 打造事件流
# 引言:异步编程的范式转换
传统的 addEventListener 方法为事件绑定回调函数虽然有效,但当我们处理复杂的、有时序的事件序列时,代码很快就会变得难以维护。
本文介绍使用 Async Generator 打造事件流,展示了如何将异步事件转换为可迭代的序列
# 核心代码:将事件转化为 Observable Stream
type ResolveFunction<T> = (value: T | PromiseLike<T>) => void; | |
class Observable { | |
// Promise 队列:存储等待被事件解决的 Promise | |
private promiseQueue: Promise<Event>[]; | |
//resolve 函数:指向当前队列头部 Promise 的解决函数 | |
private resolve: ResolveFunction<Event> | null; | |
constructor() { | |
this.promiseQueue = []; | |
this.resolve = null; | |
this.enqueue(); // 初始启动,创建第一个等待 Promise | |
} | |
private enqueue(): void { | |
// 创建一个新 Promise,并将它的 resolve 函数赋值给 this.resolve | |
this.promiseQueue.push(new Promise(resolve => this.resolve = resolve)); | |
} | |
private dequeue(): Promise<Event> | undefined { | |
// 从队列头部取出下一个 Promise | |
return this.promiseQueue.shift(); | |
} | |
// 核心方法:将 DOM 事件转化为异步生成器流 | |
async *fromEvent(element: EventTarget, eventType: string): AsyncGenerator<Event, void, unknown> { | |
// 注册事件监听器 (Push 阶段) | |
element.addEventListener(eventType, (e: Event) => { | |
// 1. 解决当前正在等待的 Promise,将事件数据推送出去 | |
if (this.resolve) { | |
this.resolve(e); | |
} | |
// 2. 立即创建下一个 Promise,等待下一个事件 | |
this.enqueue(); | |
}) | |
// 异步生成器:启动拉取流程 (Pull 阶段) | |
while (true) { | |
// 循环等待,直到队列中的下一个 Promise 被事件解决 | |
// 注意:这里需要确保 dequeue 不返回 undefined,通常通过逻辑保证队列非空 | |
const nextPromise = this.dequeue(); | |
if (nextPromise) { | |
yield await nextPromise; | |
} | |
// 如果 dequeue 返回 undefined,则无限循环会卡住,但我们在逻辑上保证了 enqueue 紧跟 resolve。 | |
} | |
} | |
} |
运行示例
(async function () { | |
const observable = new Observable(); | |
// 假设页面中存在一个 <button> 元素 | |
const button = document.querySelector('button'); | |
if (button) { | |
const clickStream = observable.fromEvent(button, 'click'); | |
// 使用 for await...of 线性处理事件 | |
for await (const e of clickStream) { | |
console.log('按钮被点击了:', e.type); | |
//... 可以在这里进行复杂的异步操作,流程依然清晰 | |
} | |
} | |
})() |
# 机制解析
利用 Promise 队列,实现了事件发生(推送)和代码处理(拉取)之间的安全同步
队列为什么时必须的?
确保时序和防止事件丢失
在任何时候,this.resolve总是指向当前等待被解决的 Promise 的解决函数。当事件以极快的速度发生时,队列提供了一个缓冲- 事件 A 触发:
resolve(A)解决了 Promise P1,并创建了 Promise P2. - 事件 B 触发:在
async generator循环来得及拉取 P2 之前,事件 B 已经触发,它安全的resolve(B)解决了 P2,并创建了 P3 - 拉取循环:循环依次恢复,先产出 A,再产出 B
如果没有队列,在快速连续事件中,第二个事件 B 极有可能找不到等待的 Promise 而被丢失。队列确保事件的发生顺序与处理顺序严格一致
- 事件 A 触发:
push 与 pull 完美结合
| 模型 | 角色 | 动作 | 描述 |
| ----- | ----- | ----- | ----- |
| push | 事件监听器 |this.resolve(e)| 事件发生时,将数据推入给正在等待的 Promise。|
| pull | 异步生成器 |yield await nextPromise| 代码处理(拉取)从队列中取出数据,确保时序。|
# 对比
| 特性 | 传统 addEventListener | Observable + async generator |
|---|---|---|
| 代码风格 | 基于回调,容易嵌套。 | 基于 for await...of 循环,流程线性化。 |
| 时序控制 | 需手动管理计数器、计时器和外部状态。 | 使用 await 关键字自然地暂停 / 等待下一个事件。 |
| 终止 / 清理 | 必须手动调用 removeEventListener 。 | 可以通过 break 退出循环,并在迭代器的 return 方法中实现自动清理(易于扩展)。 |
| 示例 | 等待 3 次点击需要外部计数器和手动清理。 | 等待 3 次点击只需一个简单的 if (count === 3) break; 。 |