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

异步队列中for await循环为何先执行dequeue再enqueue?

关于AsyncQueue与for await循环的疑问

我是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();

核心逻辑拆解

  1. for await循环的触发逻辑
    for await循环启动后,会自动调用异步迭代器的next()方法,而next()内部直接调用dequeue()。当队列空且未关闭时,dequeue()会返回一个未决议的Promise,同时把这个Promise的resolve函数存入resolvers数组。此时for await会暂停执行,等待这个Promise被决议。

  2. "先dequeue后enqueue"的设计意图
    这是AsyncQueue的核心特性——支持"预等待"。当你调用dequeue()但没有可用值时,不会抛出错误或返回空,而是先注册一个等待的resolve函数。等后续enqueue()被调用时,发现有等待的resolve,就直接用新入队的值去决议之前的Promise,让for await拿到值后继续执行循环体。这种设计完美适配了异步事件流(比如按键事件)的场景:事件发生在未来,但我们可以提前准备好接收逻辑。

  3. 队列的动态性解释
    eventStream返回的AsyncQueue确实是动态变化的:

  • 初始状态下队列是空的,但for await已经触发了dequeue(),生成了等待的Promise
  • 当按键事件触发时,enqueue()被调用,直接用事件对象决议之前的Promise,循环体拿到event后执行console.log
  • 如果连续按键,新的事件会先存入values数组,下一次循环调用dequeue()时,直接返回已决议的Promise,无需等待
  1. 循环持续等待的原理
    每完成一次循环迭代,for await会再次调用next()(也就是dequeue())。如果此时队列又空了,就会生成新的未决议Promise,继续等待下一次enqueue()。直到调用close(),所有等待的Promise都会被用EOS标记决议,循环才会终止。

内容的提问来源于stack exchange,提问作者Harpy Cutie

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 03:52:08