# 使用 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 队列,实现了事件发生(推送)和代码处理(拉取)之间的安全同步

  1. 队列为什么时必须的?
    确保时序和防止事件丢失
    在任何时候, this.resolve 总是指向当前等待被解决的 Promise 的解决函数。当事件以极快的速度发生时,队列提供了一个缓冲

    • 事件 A 触发: resolve(A) 解决了 Promise P1,并创建了 Promise P2.
    • 事件 B 触发:在 async generator 循环来得及拉取 P2 之前,事件 B 已经触发,它安全的 resolve(B) 解决了 P2,并创建了 P3
    • 拉取循环:循环依次恢复,先产出 A,再产出 B

    如果没有队列,在快速连续事件中,第二个事件 B 极有可能找不到等待的 Promise 而被丢失。队列确保事件的发生顺序与处理顺序严格一致

  2. push 与 pull 完美结合
    | 模型 | 角色 | 动作 | 描述 |
    | ----- | ----- | ----- | ----- |
    | push | 事件监听器 | this.resolve(e) | 事件发生时,将数据推入给正在等待的 Promise。|
    | pull | 异步生成器 | yield await nextPromise | 代码处理(拉取)从队列中取出数据,确保时序。|

# 对比

特性传统 addEventListenerObservable + async generator
代码风格基于回调,容易嵌套。基于 for await...of 循环,流程线性化。
时序控制需手动管理计数器、计时器和外部状态。使用 await 关键字自然地暂停 / 等待下一个事件。
终止 / 清理必须手动调用 removeEventListener可以通过 break 退出循环,并在迭代器的 return 方法中实现自动清理(易于扩展)。
示例等待 3 次点击需要外部计数器和手动清理。等待 3 次点击只需一个简单的 if (count === 3) break;