非同期イテレーションにより、オンデマンドで非同期的に取得されるデータに対してイテレートできます。例えば、ネットワーク経由でチャンク単位で何かをダウンロードする場合などです。非同期ジェネレーターを使用すると、さらに便利になります。
まずは簡単な例を見て構文を理解し、その後、実際のユースケースを確認しましょう。
イテラブルの復習
イテラブルに関するトピックを復習しましょう。
ここでは`range`のようなオブジェクトがあり、
let range = {
from: 1,
to: 5
};
…`for(value of range)`のように`for..of`ループで使用して、`1`から`5`までの値を取得したいと考えています。
言い換えれば、オブジェクトに *イテレーション機能* を追加したいということです。
`Symbol.iterator`という名前の特別なメソッドを使用して実装できます。
- このメソッドは、ループが開始されると`for..of`構成によって呼び出され、`next`メソッドを持つオブジェクトを返す必要があります。
- 各イテレーションで、次の値に対して`next()`メソッドが呼び出されます。
- `next()`は`{done: true/false, 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.iterator`の代わりに`Symbol.asyncIterator`を使用します。
- `next()`メソッドはPromiseを返す必要があります(次の値で解決されます)。
- `async`キーワードがそれを処理します。単に`async next()`とすることができます。
- このようなオブジェクトをイテレートするには、`for await (let item of iterable)`ループを使用する必要があります。
- `await`という単語に注意してください。
最初の例として、前のものと同様のイテラブル`range`オブジェクトを作成しますが、今度は1秒間に1つずつ非同期的に値を返します。
必要なのは、上記のコードでいくつかの置換を行うだけです。
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)` を持つ必要があります。
- このメソッドは、Promiseを返す`next()`メソッド `(2)` を持つオブジェクトを返す必要があります。
- `next()`メソッドは`async`である必要はありません。Promiseを返す通常のメソッドでも構いませんが、`async`を使用すると`await`を使用できるので便利です。ここでは1秒間遅延させています `(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.asyncIterator`ではなく`Symbol.iterator`を探しているからです。
`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`構成で必要とされるように、すべての値は同期的に取得される必要があります。
値を非同期的に生成したい場合はどうすればよいでしょうか?例えば、ネットワークリクエストから。
それを可能にするために、非同期ジェネレーターに切り替えましょう。
非同期ジェネレーター(ついに)
ほとんどの実用的なアプリケーションでは、非同期的に値のシーケンスを生成するオブジェクトを作成したい場合、非同期ジェネレーターを使用できます。
構文は簡単です。`function*`の前に`async`を付け加えます。これにより、ジェネレーターが非同期になります。
そして、このように`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`で動作します。
非同期イテラブルrange
通常のジェネレーターは`Symbol.iterator`として使用して、イテレーションコードを短縮できます。
これと同様に、非同期ジェネレーターは`Symbol.asyncIterator`として使用して、非同期イテレーションを実装できます。
例えば、同期的な`Symbol.iterator`を非同期的な`Symbol.asyncIterator`に置き換えることで、`range`オブジェクトが1秒間に1回非同期的に値を生成するようにすることができます。
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ユーザー)、「1ページ」を返し、次のページへの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`を要求しています。
-
コミットはJSON形式で返されます。
-
レスポンスの`Link`ヘッダーから次のページのURLを取得する必要があります。特別な形式なので、正規表現を使用します(この機能については正規表現で学習します)。
- 次のページのURLは`https://api.github.com/repositories/93253246/commits?page=2`のようなものになります。GitHub自身によって生成されます。
-
次に、受信したコミットを1つずつyieldし、完了すると次の`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..of`の代わりに`for await..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開発では、大規模なファイルのダウンロードやアップロードなど、データがチャンク単位で流れるデータストリームによく出会います。
非同期ジェネレーターを使用してこのようなデータを処理できます。また、ブラウザなどの環境では、ストリームと呼ばれる別のAPIがあり、このようなストリームを操作するための特別なインターフェースを提供し、データを変換してストリーム間で渡すことができます(例:ある場所からダウンロードしてすぐに別の場所に送信する)。
コメント
<code>
タグを使用し、複数行の場合は<pre>
タグで囲み、10行を超える場合はサンドボックス(plnkr、jsbin、codepen…)を使用してください。