Array.fromAsync 新特性实战:JS 异步数据收集的正确姿势

上周调接口翻车了。后端把原先一次返回全量数据改成了分页流,前端这边拿到的不再是普通数组,而是一个 AsyncGenerator。我第一反应是用 for await 循环一个个 push 到空数组里。写完总觉得哪里不对劲,代码又臭又长。

后来翻规范,发现 JavaScript 早就有 Array.fromAsync 了,只是自己一直没留意。用完之后当场把循环删了,写进这篇文章就是想让更多人别再走我这条弯路。

先回忆一下没有 Array.fromAsync 的时候

以前要从异步数据源里收集数组,常规写法大概是下面这样:

async function collect(asyncIterable) {
  const result = [];
  for await (const item of asyncIterable) {
    result.push(item);
  }
  return result;
}

const arr = await collect(loadPageNumbers());

功能上没问题,但每收集一次就要写一整段循环。业务里这种场景还特别多,比如分批获取数据库记录、读取文件分块、从 Kafka 消费消息。写多了会觉得这些循环都是重复劳动,和业务无关。

Array.fromAsync 是什么

Array.fromAsync 是 ES2024 新增的静态方法,作用类似 Array.from,但专门处理异步数据。它接收一个异步可迭代对象(或者任何能被转换的东西),然后返回一个 Promise,resolve 出来的就是最终数组。

基本语法:

const result = await Array.fromAsync(asyncIterable, mapFn, thisArg);

如果传入 mapFn,那每个映射函数也可以返回 Promise,Array.fromAsync 会等这个 Promise 完成后把值放进数组。这一下就把收集、转换、等待全部糅合进一个方法了。

第一个案例:从异步生成器收集团队成员

假定有个异步生成器,每次从远程接口拉取一批成员数据:

async function* fetchMembers() {
  for (let page = 1; page <= 3; page++) {
    // 假设远程接口返回一页数据
    yield await (await fetch(`/members?page=${page}`)).json();
  }
}

(async () => {
  const members = await Array.fromAsync(fetchMembers());
  console.log(members);
  // 输出类似 [[user1, user2], [user3], [user4]]
})();

注意最终得到的数组里套着子数组。如果想压平一层,直接传给 mapFn 然后展开:

const flatMembers = await Array.fromAsync(fetchMembers(), pageData => pageData);

// 并没有压平,因为 mapFn 返回值就是替进数组的元素。
// 想要压平,可以这样:
const flatMembers2 = await Array.fromAsync(
  fetchMembers(),
  pageData => pageData
).then(list => list.flat());

其实更好的做法是让异步生成器本身 yield 具体的成员,而不是整页数据。但从经验来看,后端经常会直接返回分页对象,所以自己压平的情况也很常见。

第二个案例:顺序等待一组 Promise

很多人会把 Array.fromAsync 和 Promise.all 混在一起。它们确实有点像,但行为完全不同。

Promise.all 接收的是 Promise 数组,它会并发执行所有 Promise,只要有一个失败就直接 reject。Array.fromAsync 更适合处理异步可迭代对象,它会一个接一个地等结果,顺序执行。

看个对比例子:

const tasks = [1, 2, 3].map(n => Promise.resolve(n * 10));

Promise.all(tasks).then(console.log);
// 立即输出 [10, 20, 30],但所有任务并发开始

const asyncTasks = (async function* () {
  yield new Promise(resolve => setTimeout(() => resolve('A'), 100));
  yield Promise.resolve('B');
  yield 'C';
})();

Array.fromAsync(asyncTasks).then(console.log);
// 先等100ms,再拿B和C,按顺序输出 ['A', 'B', 'C']

这个特性在“必须按顺序调用接口”的时候特别有用。比如需要先取用户信息,再根据用户 id 取订单,再根据订单取物流,用异步生成器加 Array.fromAsync 能保持代码可读。

mapFn 支持异步:数据转换的甜点

Array.fromAsync 的第二个参数 mapFn 可以返回 Promise,这意味着转换逻辑也可以异步。

比如你要把一批原始数据里的数字字段换算成格式化后的金额,但换算函数本身是异步的:

async function formatPrice(rawPrice) {
  // 模拟调用外部汇率服务
  return `$${(rawPrice * 7.2).toFixed(2)}`;
}

const products = [
  { name: '键盘', price: 10 },
  { name: '鼠标', price: 20 }
];

const formatted = await Array.fromAsync(products, async (p) => ({
  ...p,
  displayPrice: await formatPrice(p.price)
}));

console.log(formatted);
// [
//   { name: '键盘', price: 10, displayPrice: '$72.00' },
//   { name: '鼠标', price: 20, displayPrice: '$144.00' }
// ]

这里的 products 是普通数组,Array.fromAsync 也能处理普通数组,只是得到的结果依然是 Promise。用 Array.fromAsync 处理同步数组也是允许的,不会报错。

实战:分页拉取所有用户并汇总成表

再举个例子,假设有个后台系统需要到处所有用户到 CSV。用户数据通过分页接口返回,每页最多 500 条。用 AsyncGenerator 加上 Array.fromAsync,代码会非常舒服。

async function* fetchAllUsers() {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const response = await fetch(`/api/users?limit=500&page=${page}`);
    const data = await response.json();
    yield data.users;

    if (!data.hasMore) {
      hasMore = false;
    } else {
      page++;
    }
  }
}

const allUsers = (await Array.fromAsync(fetchAllUsers())).flat();

// 然后你可以把 allUsers 转成 CSV
console.log(`用户总数:${allUsers.length}`);

如果不加 Array.fromAsync,你得到的是一个二维数组,还得手动 flat。现在一行收集,一行摊平,逻辑清晰。

如果想把表格渲染到页面上,甚至可以把 mapFn 直接用上:

const userRows = (await Array.fromAsync(fetchAllUsers(), users =>
  users.map(u => ({ id: u.id, email: u.email, name: u.name }))
)).flat();

几个容易踩的坑

不能处理需要迭代器逻辑的类数组

Array.from 可以处理类数组对象,Array.fromAsync 在理论上也可以,但如果你传一个普通对象,它不像 Array.from 那样期待里面有 length 属性。Array.fromAsync 只认异步可迭代或同步可迭代对象。如果你想传 { length: 2, 0: ‘a’, 1: ‘b’ },它不会像 Array.from 那样给你 [‘a’, ‘b’]。这种情况还是先用 Array.from 转同步数组,再包一层。

mapFn 别乱 return Promise

如果你在 mapFn 里写了 return promise,Array.fromAsync 会等待。但如果你写的是箭头函数没有显式返回,那么数组里全是 undefined。这种错很好找,但初次用的时候容易翻车。

没有类似 Promise.allSettled 的容错

Array.fromAsync 那边只要有一个 Promise reject,整个 Promise 就 reject。它没有提供 allSettled 的选项。如果你希望跳过失败项目继续收集,目前还是要自己写生成器逻辑。

当前支持情况和 polyfill

这个 API 比较新,不是所有环境都能直接用。根据我的测试,Chrome 121 以后、Node 21 左右开始原生支持。普通开发环境我建议加一个 polyfill,用 core-js 很简单:

import 'core-js/proposals/array-from-async';

加了之后老环境也能跑。如果不想引入整个 core-js,自己手写一个简化版也不难,核心逻辑就是在 for await 循环里 push。

if (!Array.fromAsync) {
  Array.fromAsync = async function (iterable, mapFn) {
    const result = [];
    for await (let value of iterable) {
      if (mapFn) {
        value = await mapFn(value);
      }
      result.push(value);
    }
    return result;
  };
}

以上 polyfill 没考虑 thisArg 和同步迭代器,但应付基本场景够了。不建议生产环境直接这么用,只是帮你理解原理。

到底什么时候用 Array.fromAsync

我个人的习惯是:只要出现“从异步数据流攒数组”的冲动,第一反应就是 Array.fromAsync。尤其当异步数据源本身是 AsyncGenerator 的时候,它比手写循环更短,还省去了临时数组变量。

如果只是需要并发执行一堆 Promise,那就继续用 Promise.all;如果想要 Promise 的执行结果按顺序保留,但又不在乎并发,倒是可以试试 Array.fromAsync,不过要注意它会顺序执行,效率可能不如 Promise.all 高。

总而言之,这个新特性不是要替代 Promise.all,而是填补了“从异步迭代器构建数组”的空白。旧感觉有点像是 Array.from 和 async/await 的结合体,理解到这个层次,用起来就不会错了。

Array.fromAsync 新特性实战:JS 异步数据收集的正确姿势
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 javascript Array.fromAsync 新特性实战:JS 异步数据收集的正确姿势 https://www.taomawang.com/web/javascript/2506.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务