JS 的异步迭代器 AsyncIterable 是什么?
for await...of 不是 async 版 for...of,而是一套"随时间产生的值流"消费模型。
- 普通
for...of遍历同步数组,迭代立刻拿到值;for await...of每次迭代要等下一个值,生产者产出与消费者消费完全解耦 - 这就是流式响应的底层逻辑:Node Stream、AsyncGenerator、LLM streaming 本质上是同一件事
- 实现
Symbol.asyncIterator协议就能让任意对象变成可异步遍历的流 - Async Generator 是最简洁的生产端实现:
yield产出,for await...of消费 - 适用场景:分页 API、Stream、实时事件——凡是数据"不是一次性到齐"的地方
同步 vs 异步迭代
普通 for...of 遍历的是已经在内存里的数据。
const arr = [1, 2, 3];
for (const v of arr) {
console.log(v); // 瞬间拿完:1, 2, 3
}
迭代器 next() 同步返回 {value, done}。数组在内存里,没悬念。
for await...of 遍历的是未来才会到达的数据。
for await (const chunk of stream) {
console.log(chunk); // 等第一个 chunk 到了才打印,然后等第二个...
}
每次 next() 返回的是 Promise。循环会 await 这个 Promise,拿到了再继续下一步。
区别就一个:next() 返回值从 {value, done} 变成 Promise<{value, done}>。但这一改,整个消费模型就变了。
协议 长什么样
对比一下两种迭代协议:
| 维度 | 同步 Iterable | 异步 AsyncIterable |
|---|---|---|
| 标识符 | Symbol.iterator | Symbol.asyncIterator |
next() 返回值 | { value, done } | Promise<{ value, done }> |
| 消费语法 | for...of | for await...of |
手写一个最简实现:
const asyncIterable = {
[Symbol.asyncIterator]() {
let i = 0;
return {
async next() {
if (i < 3) {
await sleep(500); // 模拟异步数据源
return { value: i++, done: false };
}
return { done: true };
},
};
},
};
for await (const x of asyncIterable) {
console.log(x); // 0, 1, 2 —— 每个间隔 500ms
}
next() 返回 Promise,for await...of 自动 await 它,拿到 {value, done} 再决定继续还是结束。整个流程就是:等 → 拿值 → 消费 → 再等。
Async Generator 才是生产端的正确方式
手写 Symbol.asyncIterator 太啰嗦了。Async Generator 一个 yield 搞定:
async function* range(start, end, delay = 500) {
for (let i = start; i < end; i++) {
await sleep(delay);
yield i;
}
}
for await (const x of range(0, 3)) {
console.log(x);
}
Async Generator 自动实现 Symbol.asyncIterator,你只需要 yield 值。每次 yield 函数暂停,等消费者取走值后才继续跑。
yield* 还能委托给另一个 AsyncIterable,把多个数据源拼成一条流:
async function* allUsers() {
yield* fetchPage(1); // 委托给另一个 async generator
yield* fetchPage(2);
yield* fetchPage(3);
}
实际场景
分页 API 遍历。 把多页数据封装成 AsyncIterable,调用方完全不用管分页:
async function* scanAll(table, batchSize = 100) {
let cursor = null;
do {
const { items, nextCursor } = await db.scan(table, { cursor, limit: batchSize });
yield* items;
cursor = nextCursor;
} while (cursor);
}
Node.js Readable Stream。 stream.Readable 本身就实现 了 Symbol.asyncIterator:
import { createReadStream } from 'fs';
const stream = createReadStream('large.csv');
for await (const chunk of stream) {
// 逐块处理,内存友好
}
Web Streams API。 ReadableStream 同样支持异步迭代:
const response = await fetch('https://example.com/large-file');
for await (const chunk of response.body) {
// 浏览器端流式处理
}
这三种场景的共同点:数据源不是一次给到你的,是按需产出的。AsyncIterable 把"拉取下一批数据"的细节藏在迭代器背后,消费侧代码始终干净。
一个常见误区
for await...of 里的 await 不会自动并发。
// 串行:每次迭代等 500ms
for await (const x of asyncIterable) {
await process(x);
}
这和 Promise.all 没关系——AsyncIterable 本身就是按序产出的。需要并发处理的话,先收集再 batch:
const items = [];
for await (const x of asyncIterable) {
items.push(x);
}
await Promise.all(items.map(process));
总结
AsyncIterable 的意义不在于多了一种迭代语法,而是让异步数据流有了统一的消费协议。以前处理分页用回调、处理 Stream 用 on('data')、处理事件用 addEventListener,每种数据源各有一套消费方式。AsyncIterable 把这些全收拢成 for await...of——数据源自己管好"怎么产出",消费方只管"给我下一个值"。
说白了,它就是 Iterator 模式在异步世界的自然延伸。理解了 for...of 遍历数组,for await...of 遍历数据流就只是多了一个 await。
References
- for await...of —— MDN
- Symbol.asyncIterator —— MDN
- Async iteration and generators —— javascript.info
- Readable Symbol.asyncIterator —— Node.js