异步迭代器和 for await...of 适合解决什么问题?
简化版
异步迭代器让数据可以一批一批、异步地产生。for await...of 可以按顺序消费异步可迭代对象,常见于流式数据、分页拉取、Web Stream、Node Stream 等场景。
它和普通 for...of 的区别是:每次取值可能需要等待 Promise,因此天然适合处理“数据未来才到”的序列。
详细版
async function* pages() {
yield await fetchPage(1);
yield await fetchPage(2);
}
for await (const page of pages()) {
console.log(page);
}
异步可迭代对象需要实现 Symbol.asyncIterator:
const source = {
async *[Symbol.asyncIterator]() {
yield 1;
yield 2;
}
};
for await...of 会等待每次 next() 的结果,适合顺序消费,不适合默认并发。
完整版教学
一、为什么需要异步迭代
普通数组一次性存在内存中,可以同步遍历。
但接口分页、文件流、消息流不是一次性到达的。
time ->
page1 ---- page2 ---- page3
异步迭代器把“未来陆续到来的数据”抽象成可遍历序列。
异步迭代不是为了替代 Promise,而是为了表达多个异步结果组成的序列。
二、同步迭代和异步迭代的接口差异
同步迭代使用 Symbol.iterator,next() 返回普通结果。
异步迭代使用 Symbol.asyncIterator,next() 返回 Promise。
| 类型 | 协议 | next 返回 |
|---|---|---|
| 同步可迭代 | Symbol.iterator | { value, done } |
| 异步可迭代 | Symbol.asyncIterator | Promise<{ value, done }> |
这就是 for await...of 需要 await 的原因。
三、async generator 是最常见写法
async function* getUsers() {
let page = 1;
while (page <= 3) {
const users = await fetchUsers(page);
yield users;
page += 1;
}
}
async function* 同时具备 async 和 generator 的能力:可以 await,也可以 yield。
四、for await…of 顺序消费
for await (const users of getUsers()) {
render(users);
}
循环会等待当前 next() 完成,再进入下一轮。它表达的是顺序消费,而不是并发请求。
如果要并发,应该先收集任务,再使用 Promise.all 或并发控制器。
五、提前退出会触发 return
当 break、throw 或函数返回导致循环提前结束时,迭代器的 return 方法可能被调用,用于清理资源。
async function* stream() {
try {
yield 1;
yield 2;
} finally {
console.log("cleanup");
}
}
这对流和订阅类资源很重要。
六、典型业务场景
常见场景:
- 分页接口逐页加载
- ReadableStream 逐块读取
- 日志流逐条处理
- 大文件分片处理
- 消息队列消费
let count = 0;
for await (const chunk of stream) {
count += chunk.length;
}
数字上看,10MB 文件按 64KB chunk 处理,大约会产生 160 次迭代,不需要一次性把全部内容塞进内存。
七、常见误区与追问
- 误区:for await…of 会自动并发执行。 它默认按顺序等待每次迭代。
- 误区:异步迭代器只能来自 async generator。 手写
Symbol.asyncIterator也可以。 - 误区:一个 Promise 数组必须用 for await。 多数并发场景更适合
Promise.all。 - 误区:提前 break 不需要考虑清理。 迭代器可能持有流、连接或订阅资源。
- 追问:它和普通 for…of 最大区别是什么? 每次 next 可以是异步的。
- 追问:分页接口为什么适合 async generator? 它能把请求下一页和产出当前页封装在同一个序列里。
八、加强记忆
记一张图:
async source -> asyncIterator.next() -> Promise<result>
|
for await consumes
回答时先说异步序列,再说协议差异,最后落到流式读取、分页加载和顺序消费。这样比只解释语法更像真实项目经验。