异步迭代允许我们按需迭代异步生成的数据。例如,当我们通过网络分块下载某些内容时。异步生成器使这一过程更加便捷。
让我们先看一个简单的示例,以掌握语法,然后回顾一个实际用例。
回顾可迭代对象
让我们回顾一下可迭代对象这个主题。
我们的想法是,我们有一个对象,例如这里的 range
let range = {
from: 1,
to: 5
};
…我们希望对它使用 for..of
循环,例如 for(value of range)
,以获取从 1
到 5
的值。
换句话说,我们希望为对象添加一个迭代能力。
可以使用名为 Symbol.iterator
的特殊方法来实现此目的
- 当循环开始时,
for..of
构造会调用此方法,它应返回一个带有next
方法的对象。 - 对于每次迭代,都会调用
next()
方法以获取下一个值。 next()
应返回一个形式为{done: true/false, value:<loop value>}
的值,其中done:true
表示循环结束。
以下是可迭代 range
的实现
let range = {
from: 1,
to: 5,
[Symbol.iterator]() { // called once, in the beginning of for..of
return {
current: this.from,
last: this.to,
next() { // called every iteration, to get the next value
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
for(let value of range) {
alert(value); // 1 then 2, then 3, then 4, then 5
}
如果有任何不清楚的地方,请访问章节 可迭代,它提供了有关常规可迭代的所有详细信息。
异步可迭代
当值异步出现时,需要异步迭代:在 setTimeout
或其他类型的延迟之后。
最常见的情况是对象需要发出网络请求以传递下一个值,我们稍后会看到一个真实的示例。
要使对象异步可迭代
- 使用
Symbol.asyncIterator
而不是Symbol.iterator
。 next()
方法应返回一个 promise(以下一个值实现)。async
关键字会处理它,我们可以简单地编写async next()
。
- 要遍历这样的对象,我们应使用
for await (let item of iterable)
循环。- 请注意
await
字。
- 请注意
作为一个入门示例,让我们创建一个可迭代 range
对象,类似于前面的对象,但现在它将异步返回值,每秒一个。
我们只需要在上面的代码中进行一些替换
let range = {
from: 1,
to: 5,
[Symbol.asyncIterator]() { // (1)
return {
current: this.from,
last: this.to,
async next() { // (2)
// note: we can use "await" inside the async next:
await new Promise(resolve => setTimeout(resolve, 1000)); // (3)
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
(async () => {
for await (let value of range) { // (4)
alert(value); // 1,2,3,4,5
}
})()
正如我们所见,结构类似于常规迭代器
- 要使对象异步可迭代,它必须有一个方法
Symbol.asyncIterator
(1)
。 - 此方法必须返回一个带有
next()
方法的对象,该方法返回一个 promise(2)
。 next()
方法不必是async
,它可以是返回 promise 的常规方法,但async
允许我们使用await
,因此很方便。这里我们仅延迟一秒(3)
。- 要进行迭代,我们使用
for await(let value of range)
(4)
,即在 “for” 后添加 “await”。它调用range[Symbol.asyncIterator]()
一次,然后调用其next()
以获取值。
下面是一个带有差异的小表格
迭代器 | 异步迭代器 | |
---|---|---|
提供迭代器的对象方法 | Symbol.iterator |
Symbol.asyncIterator |
next() 的返回值是 |
任何值 | Promise |
要进行循环,请使用 | for..of |
for await..of |
...
异步不起作用需要常规同步迭代器的功能不适用于异步迭代器。
例如,展开语法将不起作用
alert( [...range] ); // Error, no Symbol.iterator
这是很自然的,因为它希望找到 `Symbol.iterator`,而不是 `Symbol.asyncIterator`。
对于 `for..of` 也是如此:没有 `await` 的语法需要 `Symbol.iterator`。
回顾生成器
现在让我们回顾一下生成器,因为它们可以使迭代代码更短。大多数情况下,当我们想要制作一个可迭代对象时,我们会使用生成器。
为了简单起见,省略一些重要内容,它们是“生成(yield)值的函数”。它们在 生成器 一章中有详细解释。
生成器用 `function*` 标记(注意星号),并使用 `yield` 生成一个值,然后我们可以使用 `for..of` 对它们进行循环。
此示例生成从 `start` 到 `end` 的值序列
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
for(let value of generateSequence(1, 5)) {
alert(value); // 1, then 2, then 3, then 4, then 5
}
正如我们所知,要使对象可迭代,我们应该向其中添加 `Symbol.iterator`。
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
return <object with next to make range iterable>
}
}
对于 `Symbol.iterator`,一种常见的做法是返回一个生成器,它可以使代码更短,如您所见
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
for(let value of range) {
alert(value); // 1, then 2, then 3, then 4, then 5
}
如果您想要更多详细信息,请参阅 生成器 一章。
在常规生成器中,我们不能使用 `await`。所有值都必须同步出现,正如 `for..of` 结构所要求的那样。
如果我们想要异步生成值怎么办?例如,从网络请求中。
让我们切换到异步生成器以使其成为可能。
异步生成器(最后)
对于大多数实际应用,当我们想要制作一个异步生成值序列的对象时,我们可以使用异步生成器。
语法很简单:用 `async` 预置 `function*`。这使得生成器异步。
然后使用 `for await (...)` 对其进行迭代,如下所示
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
// Wow, can use await!
await new Promise(resolve => setTimeout(resolve, 1000));
yield i;
}
}
(async () => {
let generator = generateSequence(1, 5);
for await (let value of generator) {
alert(value); // 1, then 2, then 3, then 4, then 5 (with delay between)
}
})();
由于生成器是异步的,因此我们可以在其中使用 `await`,依赖于 Promise,执行网络请求等。
从技术上讲,如果您是一位高级读者,并且还记得有关生成器的详细信息,那么内部存在差异。
对于异步生成器,`generator.next()` 方法是异步的,它返回 Promise。
在常规生成器中,我们将使用 `result = generator.next()` 来获取值。在异步生成器中,我们应该添加 `await`,如下所示
result = await generator.next(); // result = {value: ..., done: true/false}
这就是异步生成器可以与 `for await...of` 一起使用的原因。
异步可迭代范围
常规生成器可以用作 `Symbol.iterator`,以缩短迭代代码。
与此类似,异步生成器可以用作 `Symbol.asyncIterator` 来实现异步迭代。
例如,我们可以让 range
对象异步生成值,每秒一次,通过用异步 Symbol.asyncIterator
替换同步 Symbol.iterator
let range = {
from: 1,
to: 5,
// this line is same as [Symbol.asyncIterator]: async function*() {
async *[Symbol.asyncIterator]() {
for(let value = this.from; value <= this.to; value++) {
// make a pause between values, wait for something
await new Promise(resolve => setTimeout(resolve, 1000));
yield value;
}
}
};
(async () => {
for await (let value of range) {
alert(value); // 1, then 2, then 3, then 4, then 5
}
})();
现在值之间有 1 秒的延迟。
从技术上讲,我们可以将 Symbol.iterator
和 Symbol.asyncIterator
都添加到对象中,这样它既可以同步(for..of
)又可以异步(for await..of
)迭代。
但在实践中,那将是一件奇怪的事情。
真实示例:分页数据
到目前为止,我们已经看到了基本示例,以获得理解。现在让我们回顾一下现实生活中的用例。
有许多在线服务提供分页数据。例如,当我们需要用户列表时,请求会返回预定义的数量(例如 100 个用户)——“一页”,并提供指向下一页的 URL。
这种模式非常常见。它不只是关于用户,而是关于任何事情。
例如,GitHub 允许我们以相同的分页方式检索提交
- 我们应该以
https://api.github.com/repos/<repo>/commits
的形式向fetch
发出请求。 - 它用 30 个提交的 JSON 响应,并在
Link
头中提供指向下一页的链接。 - 然后我们可以使用该链接进行下一个请求,以获取更多提交,依此类推。
对于我们的代码,我们希望有一种更简单的方法来获取提交。
让我们创建一个函数 fetchCommits(repo)
,它为我们获取提交,并在需要时发出请求。并让它处理所有分页内容。对于我们来说,它将是一个简单的异步迭代 for await..of
。
因此,用法如下
for await (let commit of fetchCommits("username/repository")) {
// process commit
}
这是一个这样的函数,实现为异步生成器
async function* fetchCommits(repo) {
let url = `https://api.github.com/repos/${repo}/commits`;
while (url) {
const response = await fetch(url, { // (1)
headers: {'User-Agent': 'Our script'}, // github needs any user-agent header
});
const body = await response.json(); // (2) response is JSON (array of commits)
// (3) the URL of the next page is in the headers, extract it
let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
nextPage = nextPage?.[1];
url = nextPage;
for(let commit of body) { // (4) yield commits one by one, until the page ends
yield commit;
}
}
}
关于它如何工作的更多解释
-
我们使用浏览器的 fetch 方法下载提交。
- 初始 URL 是
https://api.github.com/repos/<repo>/commits
,下一页将在响应的Link
头中。 fetch
方法允许我们在需要时提供授权和其他头——这里 GitHub 需要User-Agent
。
- 初始 URL 是
-
提交以 JSON 格式返回。
-
我们应该从响应的
Link
头中获取下一页的 URL。它有一个特殊的格式,所以我们为此使用正则表达式(我们将在 正则表达式 中学习此功能)。- 下一页的 URL 可能看起来像
https://api.github.com/repositories/93253246/commits?page=2
。它是由 GitHub 本身生成的。
- 下一页的 URL 可能看起来像
-
然后我们一个接一个地生成接收到的提交,当它们完成时,下一个
while(url)
迭代将触发,发出一个请求。
一个使用示例(在控制台中显示提交作者)
(async () => {
let count = 0;
for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {
console.log(commit.author.login);
if (++count == 100) { // let's stop at 100 commits
break;
}
}
})();
// Note: If you are running this in an external sandbox, you'll need to paste here the function fetchCommits described above
这正是我们想要的。
分页请求的内部机制从外部是不可见的。对于我们来说,它只是一个返回提交的异步生成器。
摘要
常规迭代器和生成器适用于无需花费时间生成的数据。
当我们希望数据以异步方式延迟出现时,可以使用它们的异步对应项,使用 `for await..of` 而不是 `for..of`。
异步迭代器和常规迭代器之间的语法差异
可迭代对象 | 异步可迭代对象 | |
---|---|---|
提供迭代器的方法 | Symbol.iterator |
Symbol.asyncIterator |
next() 的返回值是 |
{value:…, done: true/false} |
解析为 `{value:…, done: true/false}` 的 `Promise` |
异步生成器和常规生成器之间的语法差异
生成器 | 异步生成器 | |
---|---|---|
声明 | function* |
async function* |
next() 的返回值是 |
{value:…, done: true/false} |
解析为 `{value:…, done: true/false}` 的 `Promise` |
在 Web 开发中,我们经常会遇到数据流,即数据块状流动。例如,下载或上传大文件。
我们可以使用异步生成器来处理此类数据。同样值得注意的是,在某些环境中,例如在浏览器中,还有另一个称为 Streams 的 API,它提供了特殊接口来处理此类流,以转换数据并将其从一个流传递到另一个流(例如从一个地方下载并立即发送到其他地方)。
评论
<code>
标记,对于多行 - 将其包装在<pre>
标记中,对于超过 10 行 - 使用沙盒 (plnkr、jsbin、codepen…)