You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何封装EventTarget.addEventListener以实现异步迭代器调用?

问题

如何使用异步迭代器消费EventTarget.addEventListener这类回调触发的事件?期望实现如下代码效果:

for await (const event of onevent('scroll')) {
  console.log(event)
  break // 执行break时自动清理事件监听器
}

尝试编写onevent封装函数时遇到两个问题:

  1. 事件回调函数内部无法使用yield
  2. 循环执行break时,没有明确的方式移除事件监听器
    示例错误代码如下:
async function *onevent(eventName) {
  const changeEvent = (event) => {
    yield event // 这里不能使用yield,因为不在生成器函数的直接作用域内
  }

   window.addEventListener(eventName, changeEvent)

   // 不知道怎么在循环break时执行清理
   // window.removeEventListener(eventName, changeEvent)
}

for await (const event of onevent('scroll')) {
  console.log(event)
  break // 如何触发清理?
}

是否误解了异步迭代器的用途?


解决方案

你并没有误解异步迭代器的用途,它完全可以用来封装这类事件流。问题出在没利用好异步迭代器的返回逻辑——当for await...of循环终止(包括break、return或抛出错误)时,迭代器的return()方法会被自动调用,我们可以在这里完成清理操作。

实现思路

  • 用队列缓存触发的事件,因为事件触发时机异步,生成器需要等待事件到来
  • 在迭代器内部绑定事件监听器,事件触发时将事件推入队列
  • 实现迭代器的return()方法,在其中移除事件监听器并标记迭代器结束

完整代码

function onevent(eventName, target = window) {
  // 缓存事件的队列
  const eventQueue = [];
  // 标记迭代器是否已终止
  let isTerminated = false;

  const eventHandler = (event) => {
    if (!isTerminated) {
      eventQueue.push(event);
      // 唤醒等待中的生成器
      resolveWaiter?.();
    }
  };

  target.addEventListener(eventName, eventHandler);

  let resolveWaiter;
  // 返回符合规范的异步迭代器
  return {
    [Symbol.asyncIterator]() {
      return this;
    },
    async next() {
      // 队列中有事件则直接返回
      if (eventQueue.length > 0) {
        return { value: eventQueue.shift(), done: false };
      }
      // 迭代器已终止则结束
      if (isTerminated) {
        return { done: true };
      }
      // 等待新事件触发
      await new Promise(resolve => {
        resolveWaiter = resolve;
      });
      // 递归调用next获取队列中的事件
      return this.next();
    },
    // 循环终止时自动调用,执行清理
    return() {
      isTerminated = true;
      target.removeEventListener(eventName, eventHandler);
      resolveWaiter?.(); // 唤醒等待的Promise,让迭代器快速结束
      return Promise.resolve({ done: true });
    }
  };
}

使用示例

(async () => {
  for await (const event of onevent('scroll')) {
    console.log('滚动事件触发:', event);
    // 执行break时,会自动调用迭代器的return()方法移除监听器
    break;
  }
})();

关键逻辑说明

  1. 事件缓存:用队列暂存异步触发的事件,保证生成器能按顺序获取事件
  2. 自动清理:return()是异步迭代器的标准方法,for await...of循环结束时JS引擎会自动调用它,我们在这里完成监听器的移除,避免内存泄漏
  3. Promise唤醒:当队列空时,生成器会等待一个Promise,事件触发时resolve该Promise,让生成器继续执行

内容的提问来源于stack exchange,提问作者David Alsh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 04:53:13