异步迭代与异步生成器
异步迭代解决什么问题?
- 场景: 值不是一次到齐, 而是等一会儿来一个, 例如分块下载、逐页请求;
- 痛点: 普通可迭代要求
next()立刻给出值, 网络请求做不到; - 方案: 把迭代协议换成异步版, 用
for await...of消费;
异步可迭代协议与普通协议有何区别?
| 项 | 可迭代 | 异步可迭代 |
|---|---|---|
| 提供迭代器的方法 | Symbol.iterator | Symbol.asyncIterator |
next() 返回值 | { value, done } | Promise<{ value, done }> |
| 遍历语法 | for...of | for await...of |
- 调用时机:
for await...of开始时调用一次[Symbol.asyncIterator](), 之后每轮对迭代器调next(); - 不通用: 需要同步迭代器的语法不会去找
Symbol.asyncIterator;
[...range]; // Error, 没有 Symbol.iterator
for (const v of range) {
} // 同理失败, 缺 await
如何手写一个异步可迭代对象?
- 三步: 定义
[Symbol.asyncIterator]; 让它返回带next()的对象; 让next()返回 Promise; - 写法:
next()不必是async, 但写成async next()才能在内部用await;
const range = {
from: 1,
to: 5,
[Symbol.asyncIterator]() {
return {
current: this.from,
last: this.to,
async next() {
await new Promise((resolve) => setTimeout(resolve, 1000)); // 每个值延迟 1s
return this.current <= this.last ? { done: false, value: this.current++ } : { done: true };
},
};
},
};
(async () => {
for await (const value of range) console.log(value); // 1 2 3 4 5, 每秒一个
})();
分块数据还有哪些处理方式?
- 适用: 下载/上传大文件这类逐块流动的数据都可用异步生成器处理;
- 浏览器 Streams: 另有专门的流接口, 可在流之间做转换与传递;
- 取舍: 只需 "逐个取用异步到达的值" 时, 异步生成器通常是最短写法;
异步生成器如何声明与驱动?
- 语法: 在
function*前加async, 得到async function*; - 能力: 函数体内可同时用
await(等异步) 与yield(产出值); - 遍历: 只能用
for await...of, 不能用for...of; 且它只能出现在 async 函数或模块顶层, 见 async 与 await; - 底层差异:
generator.next()返回 Promise, 手动取值要写await;
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
yield i;
}
}
const gen = generateSequence(1, 3);
await gen.next(); // { value: 1, done: false }
异步生成器如何实现 Symbol.asyncIterator?
- 写法: 用
async *[Symbol.asyncIterator]()方法简写, 等价于[Symbol.asyncIterator]: async function*();
const range = {
from: 1,
to: 5,
async *[Symbol.asyncIterator]() {
for (let value = this.from; value <= this.to; value++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
yield value;
}
},
};
- 可同时实现: 对象可同时挂
Symbol.iterator与Symbol.asyncIterator, 于是for...of与for await...of都能用, 但实践中少见;
如何用异步生成器处理分页数据?
- 目标: 把翻页细节藏在生成器里, 调用方只写一次
for await...of; - 思路: 循环发请求, 从响应头取下一页地址, 把当前页逐条
yield; 页内取完再请求下一页, 直到没有下一页;
async function* fetchCommits(repo) {
let url = `https://api.github.com/repos/${repo}/commits`;
while (url) {
const response = await fetch(url, { headers: { "User-Agent": "Our script" } });
const body = await response.json(); // JSON 数组, 一页若干条
// 下一页地址在 Link 响应头中
const next = response.headers.get("Link").match(/<(.*?)>; rel="next"/);
url = next?.[1];
for (const commit of body) yield commit; // 逐条产出
}
}
for await (const commit of fetchCommits("javascript-tutorial/en.javascript.info")) {
console.log(commit.author.login);
}
- 中断: 调用方
break即停止消费, 后续页不再请求;