Skip to main content

JS 的异步迭代器 AsyncIterable 是什么?

· 5 min read

for await...of 不是 async 版 for...of,而是一套"随时间产生的值流"消费模型。

  1. 普通 for...of 遍历同步数组,迭代立刻拿到值;for await...of 每次迭代要等下一个值,生产者产出与消费者消费完全解耦
  2. 这就是流式响应的底层逻辑:Node Stream、AsyncGenerator、LLM streaming 本质上是同一件事
  3. 实现 Symbol.asyncIterator 协议就能让任意对象变成可异步遍历的流
  4. Async Generator 是最简洁的生产端实现:yield 产出,for await...of 消费
  5. 适用场景:分页 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.iteratorSymbol.asyncIterator
next() 返回值{ value, done }Promise<{ value, done }>
消费语法for...offor 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

  1. for await...of —— MDN
  2. Symbol.asyncIterator —— MDN
  3. Async iteration and generators —— javascript.info
  4. Readable Symbol.asyncIterator —— Node.js