2025年,JavaScript的异步迭代器(Async Iterator)已经成为处理流式数据、大文件、实时推送的标准方案。本文通过三个真实场景的完整代码,带你掌握 for await...of、自定义异步可迭代对象、以及结合 AbortController 实现取消操作的高级用法。
1. 为什么需要异步迭代器?
传统 for...of 只能遍历同步数据,而 for await...of 可以逐个获取异步产生的值。想象一下:分页API、逐行读取大文件、WebSocket消息流——这些场景的数据是“陆续到达”的,异步迭代器让你用同步般的语法处理异步序列。
// 同步迭代:一次获取所有数据
for (const item of [1,2,3]) { console.log(item); }
// 异步迭代:逐个等待数据到达
async function* asyncGenerator() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
}
for await (const value of asyncGenerator()) {
console.log(value); // 依次输出1, 2
}
2. 核心概念:异步可迭代对象与异步生成器
任何实现了 [Symbol.asyncIterator] 方法的对象都是异步可迭代的。该方法返回一个异步迭代器,其 next() 方法返回 Promise<{value, done}>。
// 手动实现异步可迭代对象
const asyncRange = {
from: 1,
to: 3,
[Symbol.asyncIterator]() {
let current = this.from;
const max = this.to;
return {
async next() {
if (current setTimeout(r, 100));
return { value: current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};
(async () => {
for await (const num of asyncRange) {
console.log(num); // 1, 2, 3 (每个间隔100ms)
}
})();
更简洁的方式是使用 异步生成器函数 (async function*):
async function* createAsyncGenerator() {
for (let i = 1; i setTimeout(r, 100));
yield i;
}
}
(async () => {
for await (const val of createAsyncGenerator()) {
console.log(val);
}
})();
3. 实战案例一:分页API的流式加载
很多RESTful API采用分页返回数据。我们可以封装一个异步迭代器,自动请求下一页,直到数据耗尽。这样业务代码只需 for await...of 即可逐条处理数据。
// 模拟分页API
async function fetchPage(pageNum, pageSize = 5) {
// 模拟网络延迟
await new Promise(r => setTimeout(r, 200));
const totalItems = 12; // 总共12条数据
const start = (pageNum - 1) * pageSize;
const end = Math.min(start + pageSize, totalItems);
if (start >= totalItems) return { data: [], hasMore: false };
const data = [];
for (let i = start; i < end; i++) {
data.push({ id: i + 1, name: `商品${i + 1}` });
}
return { data, hasMore: end 0) {
return { value: buffer.shift(), done: false };
}
// 如果没有更多页,结束迭代
if (!hasMore) {
return { value: undefined, done: true };
}
// 请求下一页
const result = await fetchPage(pageNum, pageSize);
hasMore = result.hasMore;
pageNum++;
buffer = result.data;
if (buffer.length === 0) {
return { value: undefined, done: true };
}
return { value: buffer.shift(), done: false };
}
};
}
};
}
// 使用示例
(async () => {
console.log('开始流式加载分页数据:');
for await (const item of paginatedLoader(5)) {
console.log('收到:', item.name);
}
console.log('所有数据加载完成');
})();
4. 实战案例二:逐行读取大文件(浏览器环境)
浏览器中处理大文件(如1GB日志)不能一次性读入内存。利用 FileReader 和异步迭代器,我们可以逐行读取并处理。
// 逐行读取文件流
function readLinesAsAsyncIterator(file) {
const chunkSize = 1024 * 64; // 64KB
let offset = 0;
let remainder = ''; // 跨块残留的行尾
return {
[Symbol.asyncIterator]() {
return {
async next() {
if (offset >= file.size && remainder === '') {
return { value: undefined, done: true };
}
// 读取下一块
const blob = file.slice(offset, offset + chunkSize);
offset += chunkSize;
const text = await blob.text();
const lines = (remainder + text).split('n');
remainder = lines.pop() || ''; // 最后一段可能不完整,留到下次
// 返回第一行(如果有)
if (lines.length > 0) {
return { value: lines[0], done: false };
}
// 如果没有完整行,继续读取下一块
return this.next();
}
};
}
};
}
// 使用示例(假设用户选择了文件)
// document.getElementById('fileInput').addEventListener('change', async (e) => {
// const file = e.target.files[0];
// let lineCount = 0;
// for await (const line of readLinesAsAsyncIterator(file)) {
// lineCount++;
// if (lineCount <= 5) console.log('行内容:', line);
// }
// console.log(`总行数: ${lineCount}`);
// });
5. 实战案例三:实时数据流与取消控制
结合 AbortController,我们可以优雅地取消异步迭代。下面模拟一个WebSocket消息流,支持外部中断。
// 模拟实时消息流(可取消)
function createMessageStream(signal) {
let counter = 0;
const maxMessages = 20;
return {
[Symbol.asyncIterator]() {
return {
async next() {
// 检查是否被取消
if (signal?.aborted) {
return { value: undefined, done: true };
}
if (counter >= maxMessages) {
return { value: undefined, done: true };
}
// 模拟消息到达(随机延迟)
await new Promise(r => setTimeout(r, 300 + Math.random() * 500));
// 再次检查取消(在等待期间可能被取消)
if (signal?.aborted) {
return { value: undefined, done: true };
}
counter++;
return {
value: { id: counter, content: `消息 #${counter}`, timestamp: Date.now() },
done: false
};
}
};
}
};
}
// 使用AbortController取消
(async () => {
const controller = new AbortController();
const signal = controller.signal;
// 5秒后自动取消
setTimeout(() => {
console.log('>>> 取消信号发出');
controller.abort();
}, 3000);
console.log('开始接收实时消息:');
try {
for await (const msg of createMessageStream(signal)) {
console.log('收到:', msg.content);
}
} catch (e) {
if (e.name === 'AbortError') {
console.log('迭代被取消');
} else {
console.error('错误:', e);
}
}
console.log('流已关闭');
})();
6. 性能与错误处理最佳实践
- 错误传播:异步生成器内部抛出的异常会传递给
for await...of,用try/catch包裹即可。 - 并发控制:异步迭代器天然是顺序的,如果需要并发拉取,可以在生成器内部使用
Promise.all批量获取。 - 内存安全:处理无限流时,确保消费速度大于生产速度,否则缓冲区会膨胀。可以使用背压(backpressure)机制。
- 与RxJS对比:异步迭代器更轻量,适合顺序消费;RxJS适合复杂变换和组合。
// 错误处理示例
async function* faultyGenerator() {
yield 1;
throw new Error('模拟错误');
yield 2; // 不会执行
}
(async () => {
try {
for await (const val of faultyGenerator()) {
console.log(val);
}
} catch (err) {
console.error('捕获错误:', err.message);
}
})();
7. 浏览器与Node.js环境差异
| 环境 | 支持情况 | 典型应用 |
|---|---|---|
| 现代浏览器 (Chrome 90+) | 完全支持 for await...of 和异步生成器 |
File API 逐行读取、Fetch API 流式响应 |
| Node.js 18+ | 完全支持,且内置 fs.createReadStream 可转为异步迭代 |
大文件处理、数据库游标、HTTP流 |
Node.js 中可以将 Readable 流转为异步迭代器:
// Node.js 示例 (需要 Node 18+)
// import { createReadStream } from 'fs';
// async function* readFileLines(path) {
// const stream = createReadStream(path, { encoding: 'utf-8' });
// let remainder = '';
// for await (const chunk of stream) {
// const lines = (remainder + chunk).split('n');
// remainder = lines.pop();
// for (const line of lines) yield line;
// }
// if (remainder) yield remainder;
// }
8. 总结
异步迭代器是JavaScript处理流式数据的“瑞士军刀”。通过本文的案例,你学会了:
- 自定义异步可迭代对象和异步生成器
- 分页API的流式加载,避免一次性请求大量数据
- 浏览器中大文件的逐行读取
- 结合AbortController实现可取消的实时数据流
在2025年的前端/Node.js开发中,异步迭代器已经成为处理数据流的首选模式。掌握它,让你的代码更优雅、内存更高效。
本文原创,基于ECMAScript 2025标准。所有代码均可在Chrome 110+或Node.js 20+中直接运行。

