跳到主要内容

异步迭代与异步生成器

异步迭代解决什么问题?​

  • 场景: 值不是一次到齐, 而是等一会儿来一个, 例如分块下载、逐页请求;
  • 痛点: 普通可迭代要求 next() 立刻给出值, 网络请求做不到;
  • 方案: 把迭代协议换成异步版, 用 for await...of 消费;

异步可迭代协议与普通协议有何区别?​

项可迭代异步可迭代
提供迭代器的方法Symbol.iteratorSymbol.asyncIterator
next() 返回值{ value, done }Promise<{ value, done }>
遍历语法for...offor 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 即停止消费, 后续页不再请求;