上周调接口翻车了。后端把原先一次返回全量数据改成了分页流,前端这边拿到的不再是普通数组,而是一个 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 的结合体,理解到这个层次,用起来就不会错了。

