异步队列中for await循环为何先执行dequeue再enqueue?
我是JavaScript新手,正在阅读David Flanagan的指南学习基础知识。这段AsyncQueue相关的代码让我感到困惑:我无法理解for await循环中的Promise为何会在被enqueue(入队)前就被dequeue(出队)?
首先我们通过eventStream创建AsyncQueue并监听按键事件,事件触发时才会将事件对象enqueue;但之后的for await循环却直接开始迭代队列中的值。我疑惑的是,队列中还没有事件时,循环里的event从何而来?难道for await循环会在Promise入队前就执行dequeue操作?for await不是应该等待下一个Promise被resolve吗?是不是eventStream返回的队列是动态变化的?
完整代码如下:
/** * An asynchronously iterable queue class. Add values with enqueue() * and remove them with dequeue(). dequeue() returns a Promise, which * means that values can be dequeued before they are enqueued. The * class implements [Symbol.asyncIterator] and next() so that it can * be used with the for/await loop (which will not terminate until * the close() method is called.) */ class AsyncQueue { constructor() { // Values that have been queued but not dequeued yet are stored here this.values = []; // When Promises are dequeued before their corresponding values are // queued, the resolve methods for those Promises are stored here. this.resolvers = []; // Once closed, no more values can be enqueued, and no more unfulfilled // Promises returned. this.closed = false; } enqueue(value) { if (this.closed) { throw new Error("AsyncQueue closed"); } if (this.resolvers.length > 0) { // If this value has already been promised, resolve that Promise const resolve = this.resolvers.shift(); resolve(value); } else { // Otherwise, queue it up this.values.push(value); } } dequeue() { if (this.values.length > 0) { // If there is a queued value, return a resolved Promise for it const value = this.values.shift(); return Promise.resolve(value); } else if (this.closed) { // If no queued values and we're closed, return a resolved // Promise for the "end-of-stream" marker return Promise.resolve(AsyncQueue.EOS); } else { // Otherwise, return an unresolved Promise, // queuing the resolver function for later use return new Promise((resolve) => { this.resolvers.push(resolve); }); } } close() { // Once the queue is closed, no more values will be enqueued. // So resolve any pending Promises with the end-of-stream marker while (this.resolvers.length > 0) { this.resolvers.shift()(AsyncQueue.EOS); } this.closed = true; } // Define the method that makes this class asynchronously iterable [Symbol.asyncIterator]() { return this; } // Define the method that makes this an asynchronous iterator. The // dequeue() Promise resolves to a value or the EOS sentinel if we're // closed. Here, we need to return a Promise that resolves to an // iterator result object. next() { return this.dequeue().then((value) => value === AsyncQueue.EOS ? { value: undefined, done: true } : { value: value, done: false } ); } } // A sentinel value returned by dequeue() to mark "end of stream" when closed AsyncQueue.EOS = Symbol("end-of-stream"); // Push events of the specified type on the specified document element // onto an AsyncQueue object, and return the queue for use as an event stream function eventStream(elt, type) { const q = new AsyncQueue(); // Create a queue elt.addEventListener(type, (e) => q.enqueue(e)); // Enqueue events return q; } async function handleKeys() { // Get a stream of keypress events and loop once for each one for await (const event of eventStream(document, "keypress")) { console.log(event.key); } } let a = handleKeys();
核心逻辑拆解
for await循环的触发逻辑
for await循环启动后,会自动调用异步迭代器的next()方法,而next()内部直接调用dequeue()。当队列空且未关闭时,dequeue()会返回一个未决议的Promise,同时把这个Promise的resolve函数存入resolvers数组。此时for await会暂停执行,等待这个Promise被决议。"先dequeue后enqueue"的设计意图
这是AsyncQueue的核心特性——支持"预等待"。当你调用dequeue()但没有可用值时,不会抛出错误或返回空,而是先注册一个等待的resolve函数。等后续enqueue()被调用时,发现有等待的resolve,就直接用新入队的值去决议之前的Promise,让for await拿到值后继续执行循环体。这种设计完美适配了异步事件流(比如按键事件)的场景:事件发生在未来,但我们可以提前准备好接收逻辑。队列的动态性解释
eventStream返回的AsyncQueue确实是动态变化的:
- 初始状态下队列是空的,但for await已经触发了
dequeue(),生成了等待的Promise - 当按键事件触发时,
enqueue()被调用,直接用事件对象决议之前的Promise,循环体拿到event后执行console.log - 如果连续按键,新的事件会先存入
values数组,下一次循环调用dequeue()时,直接返回已决议的Promise,无需等待
- 循环持续等待的原理
每完成一次循环迭代,for await会再次调用next()(也就是dequeue())。如果此时队列又空了,就会生成新的未决议Promise,继续等待下一次enqueue()。直到调用close(),所有等待的Promise都会被用EOS标记决议,循环才会终止。
内容的提问来源于stack exchange,提问作者Harpy Cutie

