如何封装EventTarget.addEventListener以实现异步迭代器调用?
问题
如何使用异步迭代器消费EventTarget.addEventListener这类回调触发的事件?期望实现如下代码效果:
for await (const event of onevent('scroll')) { console.log(event) break // 执行break时自动清理事件监听器 }
尝试编写onevent封装函数时遇到两个问题:
- 事件回调函数内部无法使用
yield - 循环执行
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; } })();
关键逻辑说明
- 事件缓存:用队列暂存异步触发的事件,保证生成器能按顺序获取事件
- 自动清理:
return()是异步迭代器的标准方法,for await...of循环结束时JS引擎会自动调用它,我们在这里完成监听器的移除,避免内存泄漏 - Promise唤醒:当队列空时,生成器会等待一个Promise,事件触发时resolve该Promise,让生成器继续执行
内容的提问来源于stack exchange,提问作者David Alsh
相关产品推荐
相关产品推荐

