For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/proposal-array-from-async.md.
  • 简体中文
  • Array.fromAsync S4

    提案概览
    提案速览

    该提案添加了一个 Array.fromAsync 静态方法,用于将异步可迭代对象、同步可迭代对象和类数组对象异步转换为数组,类似于 Array.from 但面向异步迭代。它支持可选的映射函数和 thisArg,匹配 for await 的语义,包括惰性迭代和错误处理,并且是一个泛型工厂方法。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    用于 JavaScript 的 Array.fromAsync

    ECMAScript 第 4 阶段(待编辑审查)提案。J. S. Choi,2021–2025。

    为什么需要 Array.fromAsync 方法

    自从在 JavaScript 中标准化以来,Array.from 已成为 Array 最常用的内置方法之一。然而,对于异步迭代器,尚不存在类似的功能。

    const arr = [];
    for (const v of iterable) {
      arr.push(v);
    }
    
    // 这做同样的事情。
    const arr = Array.from(iterable);

    这样的功能对于将异步迭代器全部内容转储到单个数据结构中也很有用,尤其是在单元测试命令行界面中。(后面章节包含几个真实世界的示例。)

    const arr = [];
    for await (const v of asyncIterable) {
      arr.push(v);
    }
    
    // 我们应添加能完成同样事情的功能。
    const arr = await ??????????(asyncIterable);

    有一个 it-all NPM 库仅执行此任务,每周下载量约 50,000 次。这当然包括任何使用带有空数组的临时 for awaitof 循环的代码。进一步证明了对此类功能的需求,多个 Stack Overflow 问题 已经由不同的开发者提出,询问如何将异步迭代器转换为数组。

    本解释器的后面列出了几个 真实世界的示例

    描述

    (可获取正式草案规范。)

    Array.fromAsync 之于 for await
    正如 Array.from 之于 for

    Array.from 类似,Array.fromAsync 将是 Array 内置类的静态方法,带有一个必需参数和两个可选参数:(items, mapfn, thisArg)

    异步可迭代输入

    但是,Array.fromAsync 不是将同步可迭代对象转换为数组,而是可以将异步可迭代对象转换为promise,该 promise(如果一切顺利)将解析为一个新数组。在 promise 解析之前,它将从输入创建一个异步迭代器,惰性迭代它,并将每个产生的值添加到新数组中。(无论如何,promise 在 Array.fromAsync 函数调用后立即返回。)

    async function * asyncGen (n) {
      for (let i = 0; i < n; i++)
        yield i * 2;
    }
    
    // `arr` 将是 `[0, 2, 4, 6]`。
    const arr = [];
    for await (const v of asyncGen(4)) {
      arr.push(v);
    }
    
    // 这是等价的。
    const arr = await Array.fromAsync(asyncGen(4));

    同步可迭代输入

    如果参数是同步可迭代对象(而不是异步可迭代对象),则返回值仍然是一个将解析为数组的 promise。如果同步迭代器产生 promises,则每个产生的 promise 在将其值添加到新数组之前都会被等待。(非 promise 的值也会被等待,以防止 Zalgo。)所有这些都符合 for await 的行为。

    function * genPromises (n) {
      for (let i = 0; i < n; i++)
        yield Promise.resolve(i * 2);
    }
    
    // `arr` 将是 `[ 0, 2, 4, 6 ]`。
    const arr = [];
    for await (const v of genPromises(4)) {
      arr.push(v);
    }
    
    // 这是等价的。
    const arr = await Array.fromAsync(genPromises(4));

    for await 一样,Array.fromAsync 对同步但非异步的输入进行惰性迭代。每当开发者需要将产生 promises 的同步输入转储到数组中时,开发者需要在 Array.fromAsync 和 Promise.all 之间谨慎选择,它们具有互补的控制流:

    并行等待 顺序等待
    惰性迭代 不可能 await Array.fromAsync(input)
    急切迭代 await Promise.all(Array.from(input)) 无用

    同样与 for await 一样,当给定同步但非异步的可迭代输入时,Array.fromAsync 将捕获其迭代到达的第一个拒绝,并且仅当该拒绝发生在迭代到达并等待它之前的微任务中。更多信息请参见 § 错误

    // `arr` 将是 `[ 0, 2, 4, 6 ]`。
    // `genPromises(4)` 被惰性迭代,
    // 其四个产生的 promises 按顺序被等待。
    const arr = await Array.fromAsync(genPromises(4));
    
    // `arr` 也将是 `[ 0, 2, 4, 6 ]`。
    // 然而,`genPromises(4)` 被急切迭代
    // (变为四个 promises 的数组),
    // 并且这四个 promises 被并行等待。
    const arr = await Promise.all(Array.from(genPromises(4)));

    不可迭代的类数组输入

    Array.fromAsync 的有效输入是 Array.from 有效输入的超集。这包括不可迭代的类数组:具有 length 属性以及索引元素的对象(类似于 Array.prototype.values)。返回值仍然是一个将解析为数组的 promise。如果类数组对象的元素是 promises,则每个访问到的 promise 在将其值添加到新数组之前都会被等待。

    一位 TC39 代表的意见:“[类数组]不会过时,而且非常不错的是,事物不必强制实现迭代器协议就能转换为数组。”

    const arrLike = {
      length: 4,
      0: Promise.resolve(0),
      1: Promise.resolve(2),
      2: Promise.resolve(4),
      3: Promise.resolve(6),
    }
    
    // `arr` 将是 `[ 0, 2, 4, 6 ]`。
    const arr = [];
    for await (const v of Array.from(arrLike)) {
      arr.push(v);
    }
    
    // 这是等价的。
    const arr = await Array.fromAsync(arrLike);

    与其对同步但非异步的可迭代输入所做的一样,Array.fromAsync 惰性迭代类数组输入的值,并等待每个值。开发者必须在 Array.fromAsync 和 Promise.all 之间进行选择(参见 § 同步可迭代输入§ 错误)。

    泛型工厂方法

    Array.fromAsync 是一个泛型工厂方法。它不要求其 this 接收者是 Array 构造函数。fromAsync 可以被任何其他构造函数转移或继承。在这种情况下,最终结果将是由该构造函数(不传参数)创建的数据结构,并将输入产生的每个值赋给该数据结构的数字属性。(完全不涉及 Symbol.species。)如果 this 接收者不是构造函数,则 fromAsync 照常创建数组。这符合 Array.from 的行为。

    async function * asyncGen (n) {
      for (let i = 0; i < n; i++)
        yield i * 2;
    }
    function Data (n) {}
    Data.from = Array.from;
    Data.fromAsync = Array.fromAsync;
    
    // d 将是 `new Data(0)`,其 `0` 属性赋值为 `0`,其 `1` 属性赋值为 `2`,等等。
    const d = new Data(0); let i = 0;
    for await (const v of asyncGen(4)) {
      d[i++] = v;
    }
    
    // 这是等价的。
    const d = await Data.fromAsync(asyncGen(4));

    可选参数

    Array.fromAsync 有两个可选参数:mapfnthisArg

    映射函数

    mapfn 是一个可选的映射回调,它对输入产生的每个值调用,并附带其索引整数(从 0 开始)。然后,映射回调的每个结果依次被等待,然后添加到数组中。

    然而,当提供了 mapfn 且输入是同步可迭代对象(或不可迭代的类数组)时,输入中的每个值在被传递给 mapfn 之前都会被等待。(如果输入是异步可迭代对象,则输入中的值被等待。)这符合 for await 的行为。

    当未提供 mapfn 时,来自异步输入的每个产生的值不被等待,而来自同步输入的每个产生的值在添加到结果数组之前只被等待一次。这也符合 for await 的行为。

    这意味着:

    Array.fromAsync(input)

    …不等价于:

    Array.fromAsync(input, x => x)

    …至少当 input 是异步可迭代对象时是这样。

    这是因为,每当输入是产生 promise 项的异步可迭代对象时,Array.fromAsync(input) 不会解析这些 promise 项,但 Array.fromAsync(input, x => x) 会解析它们,因为 x => x 映射函数的结果被等待。

    例如:

    function createAsyncIter () {
      let i = 0;
      return {
        [Symbol.asyncIterator]() {
          return {
            async next() {
              if (i > 2) return { done: true };
              i++;
              return { value: Promise.resolve(i), done: false }
            }
          }
        }
      };
    }
    
    // 这打印 `[Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]`:
    console.log(await Array.fromAsync(createAsyncIter()));
    
    // 这打印 `[1, 2, 3]`:
    console.log(await Array.fromAsync(createAsyncIter(), x => x));

    另请参见 issue #19

    this 参数

    thisArg 是映射回调的 this 绑定接收值。默认情况下为 undefined。这些可选参数与 Array.from 的行为一致。将它们排除会让已经习惯 Array.from 的开发者感到意外。

    async function * asyncGen (n) {
      for (let i = 0; i < n; i++)
        yield i * 2;
    }
    
    // `arr` 将是 `[ 0, 4, 16, 36 ]`。
    const arr = [];
    for await (const v of asyncGen(4)) {
      arr.push(await (v ** 2));
    }
    
    // 这是等价的。
    const arr = await Array.fromAsync(asyncGen(4), v =>
      v ** 2);

    错误

    与其他基于 promise 的 API 一样,Array.fromAsync 将始终立即返回一个 promise。Array.fromAsync 永远不会同步抛出错误和召唤 Zalgo

    当 Array.fromAsync 的输入在创建其异步或同步迭代器时抛出错误,Array.fromAsync 返回的 promise 将以该错误拒绝。

    const err = new Error;
    const badIterable = { [Symbol.iterator] () { throw err; } };
    
    // 这返回一个将用 `err` 拒绝的 promise。
    Array.fromAsync(badIterable);

    当 Array.fromAsync 的输入是可迭代的,但输入的迭代器在迭代时抛出错误,Array.fromAsync 返回的 promise 将以该错误拒绝。

    const err = new Error;
    async function * genErrorAsync () { throw err; }
    
    // 这返回一个将用 `err` 拒绝的 promise。
    Array.fromAsync(genErrorAsync());
    const err = new Error;
    function * genError () { throw err; }
    
    // 这返回一个将用 `err` 拒绝的 promise。
    Array.fromAsync(genError());

    当 Array.fromAsync 的输入仅是同步的(即,输入不是异步可迭代对象),并且当输入的一个值是一个最终会拒绝或已经拒绝的 promise 时,迭代停止,Array.fromAsync 返回的 promise 将用第一个此类错误拒绝。

    在这种情况下,Array.fromAsync 将仅当该拒绝发生在迭代到达并等待它之前的微任务中时,才捕获并处理该第一个输入拒绝。

    const err = new Error;
    function * genRejection () {
      yield Promise.reject(err);
    }
    
    // 这返回一个将用 `err` 拒绝的 promise。**没有**未处理的 promise 拒绝,因为拒绝发生在同一个微任务中。
    Array.fromAsync(genZeroThenRejection());

    就像 for await 一样,当输入的 promises 的拒绝发生在 Array.fromAsync 的迭代到达这些 promises 之前,Array.fromAsync 将捕获这些拒绝。

    这是因为 —— 像 for await 一样 —— Array.fromAsync 惰性迭代其输入,并顺序等待每个产生的值。每当开发者需要将产生 promises 的同步输入转储到数组中时,开发者需要在 Array.fromAsync 和 Promise.all 之间谨慎选择,它们具有互补的控制流(请参见 § 同步可迭代输入)。

    例如,当同步输入包含两个 promises,后者在前者 promise 解析之前拒绝,Array.fromAsync 将不捕获该拒绝,因为它惰性地到达拒绝的 promise 时,它已经拒绝了。

    const numOfMillisecondsPerSecond = 1000;
    const slowError = new Error;
    const fastError = new Error;
    
    function waitThenReject (value) {
      return new Promise((resolve, reject) => {
        setTimeout(() => reject(value), numOfMillisecondsPerSecond);
      });
    }
    
    function * genRejections () {
      // 慢 promise。
      yield waitAndReject(slowError);
      // 快 promise。
      yield Promise.reject(fastError);
    }
    
    // 这返回一个将用 `slowError` 拒绝的 promise。**没有**未处理的 promise 拒绝:迭代是惰性的,将在慢 promise 处提前停止,因此快 promise 永远不会被创建。
    Array.fromAsync(genSlowRejectThenFastReject());
    
    // 这返回一个将用 `slowError` 拒绝的 promise。**有**一个未处理的 promise 拒绝,带 `fastError`:迭代急切地创建并将两个 promises 转储到数组中,但 Array.fromAsync 将**顺序**只处理慢 promise。
    Array.fromAsync([ ...genSlowRejectThenFastReject() ]);
    
    // 这返回一个将用 `fastError` 拒绝的 promise。**没有**未处理的 promise 拒绝:迭代急切地创建并将两个 promises 转储到数组中,但 Promise.all 将**并行**处理两个 promises。
    Promise.all([ ...genSlowRejectThenFastReject() ]);

    当 Array.fromAsync 的输入至少有一个值,并且当 Array.fromAsync 的映射回调在给定这些值中的任何一个时抛出错误,Array.fromAsync 返回的 promise 将用第一个此类错误拒绝。

    const err = new Error;
    function badCallback () { throw err; }
    
    // 这返回一个将用 `err` 拒绝的 promise。
    Array.fromAsync([ 0 ], badCallback);

    当 Array.fromAsync 的输入为 null 或 undefined,或者当 Array.fromAsync 的映射回调既不是 undefined 也不可调用时,Array.fromAsync 返回的 promise 将用 TypeError 拒绝。

    // 这些返回将用 TypeError 拒绝的 promises。
    Array.fromAsync(null);
    Array.fromAsync([], 1);

    关闭同步可迭代对象?

    Array.fromAsync 尝试尽可能匹配 for await 的行为。

    以前,当 for await 产生一个被拒绝的 promise 时,它不会关闭同步可迭代对象。

    旧代码示例
    function * createIter() {
      try {
        yield Promise.resolve(console.log("a"));
        yield Promise.reject("x");
      } finally {
        console.log("finalized");
      }
    }
    
    // 打印 "a",然后打印 "finalized"。
    // 有一个未捕获的 "x" 拒绝。
    for (const x of createIter()) {
      console.log(await x);
    }
    
    // 打印 "a",然后打印 "finalized"。
    // 有一个未捕获的 "x" 拒绝。
    Array.from(createIter());
    
    // 打印 "a",不打印 "finalized"。
    // 有一个未捕获的 "x" 拒绝。
    for await (const x of createIter()) {
      console.log(x);
    }
    
    // 打印 "a",不打印 "finalized"。
    // 有一个未捕获的 "x" 拒绝。
    Array.fromAsync(createIter());

    TC39 最近更改了 for await 在这里的行为。 在最新版本的语言中, 当异步包装器产生拒绝时,for await 现在将关闭同步迭代器(参见 tc39/ecma262#2600)。 所有 JavaScript 引擎都已经在更新到这种新行为。

    Array.fromAsync 匹配 for await 的这种新行为。 两者都会在同步迭代器产生一个被拒绝的 promise 作为其下一个值时关闭任何给定的同步迭代器。

    其他提案

    与迭代器辅助方法的关系

    iterator-helpersasync-iterator-helpers 提案定义了 Iterator.toArray 和 AsyncIterator.toArray。以下成对的行是等价的:

    // Array.from
    
    Array.from(iterable)
    Iterator(iterable).toArray()
    
    Array.from(iterable, mapfn)
    Iterator(iterable).map(mapfn).toArray()
    
    // Array.fromAsync
    
    Array.fromAsync(asyncIterable)
    AsyncIterator(asyncIterable).toArray()
    
    Array.fromAsync(asyncIterable, mapfn)
    AsyncIterator(asyncIterable).map(mapfn).toArray()

    Iterator.toArray 与 Array.from 重叠,AsyncIterator.toArray 与 Array.fromAsync 重叠。这没关系:它们都可以共存。

    [iterable-helpers 的共同发起人][tc39/proposal-iterator-helpers#156] 同意我们应该同时拥有两者,或者我们应该优先考虑 Array.fromAsync:“我记得为什么对于一个可构建的结构来说,消费一个可迭代对象比一个可迭代对象消费一个可构建协议更好。有时一次构建一个元素等同于一次构建[多个]元素,但有时这样构建可能会很慢,或者产生一个具有相同语义但性能特性不同的结构。”

    [tc39/proposal-iterator-helpers#156]: https://github.com/tc39/proposal-iterator-helpers/issues/156. 协议

    TypedArray.fromAsync、Set.fromAsync、Object.fromEntriesAsync 等

    以下内置方法也类似于 Array.from:

    TypedArray.from()
    new Set
    Object.fromEntries()
    new Map

    我们将这些方法的任何异步版本推迟到未来的提案。 请参见 issue #8proposal-setmap-offrom

    异步展开运算符

    未来,标准化异步展开运算符(如 [ 0, await ...v ])可能会很有用。本提案将这一想法留给单独的提案。

    记录和元组

    record/tuple 提案提出了两种新的数据类型,其 API 分别类似于 ArrayObject 的 API。Tuple 构造函数也可能需要一个 fromAsync 方法。Record 构造函数是否获得 fromEntriesAsync 方法将取决于 Object.fromEntriesAsync 是否也将在单独的提案中添加。

    真实世界示例

    仅对现状示例做了轻微格式更改。

    现状 使用 Array.fromAsync
    const all = require('it-all');
    
    // 将默认资源添加到仓库。
    const results = await all(
      addAll(
        globSource(initDocsPath, {
          recursive: true,
        }),
        { preload: false },
      ),
    );
    const dir = results
      .filter(file =>
        file.path === 'init-docs')
      .pop()
    print('to get started, enter:\n');
    print(
      `\tjsipfs cat` +
      `/ipfs/${dir.cid}/readme\n`,
    );

    来自 ipfs-core/src/runtime/init-assets-nodejs.js

    // 将默认资源添加到仓库。
    const results = await Array.fromAsync(
      addAll(
        globSource(initDocsPath, {
          recursive: true,
        }),
        { preload: false },
      ),
    );
    const dir = results
      .filter(file =>
        file.path === 'init-docs')
      .pop()
    print('to get started, enter:\n');
    print(
      `\tjsipfs cat` +
      `/ipfs/${dir.cid}/readme\n`,
    );
    const all = require('it-all');
    
    const results = await all(
      node.contentRouting
        .findProviders('a cid'),
    );
    expect(results)
      .to.be.an('array')
      .with.lengthOf(1)
      .that.deep.equals([result]);

    来自 js-libp2p/test/content-routing/content-routing.node.js

    const results = await Array.fromAsync(
      node.contentRouting
        .findProviders('a cid'),
    );
    expect(results)
      .to.be.an('array')
      .with.lengthOf(1)
      .that.deep.equals([result]);
    async function toArray(items) {
      const result = [];
      for await (const item of items) {
        result.push(item);
      }
      return result;
    }
    
    it('empty-pipeline', async () => {
      const pipeline = new Pipeline();
      const result = await toArray(
        pipeline.execute(
          [ 1, 2, 3, 4, 5 ]));
      assert.deepStrictEqual(
        result,
        [ 1, 2, 3, 4, 5 ],
      );
    });

    来自 node-httptransfer/test/generator/pipeline.test.js

    it('empty-pipeline', async () => {
      const pipeline = new Pipeline();
      const result = await Array.fromAsync(
        pipeline.execute(
          [ 1, 2, 3, 4, 5 ]));
      assert.deepStrictEqual(
        result,
        [ 1, 2, 3, 4, 5 ],
      );
    });