非同期イテレーションを使用すると、要求に応じて非同期に来るデータに対して反復処理することができます。例えば、ネットワーク経由でチャンクごとに何かをダウンロードする場合です。そして、非同期ジェネレータはそれをさらに便利にします。
最初に単純な例を見て構文を把握し、その後、実際のユースケースを見ていきましょう。
反復可能(iterable)を思い出してください
反復可能(iterable)についてのトピックを思い出しましょう。
ここでは 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:<loop value>}の形式の値の返却が必要で、done:trueはループが終わりであることを意味します。
以下は、反復可能な range の実装です:
let range = {
from: 1,
to: 5,
[Symbol.iterator]() { // for..of の最初に一度呼ばれます
return {
current: this.from,
last: this.to,
next() { // 各イテレーションで呼ばれ、次の値を取得します
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秒毎に値を非同期的に返します。:
やるべきことは、上のコードに対し少し置き換えるだけです:
let range = {
from: 1,
to: 5,
[Symbol.asyncIterator]() { // (1)
return {
current: this.from,
last: this.to,
async next() { // (2)
// async next の中で、 "await" が使えます
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 を必要とします。
Recall generators
ここでジェネレータを思い出しましょう。ジェネレータを使用すると、イテレーションのコードをはるかに短くすることができます。ほとんどの場合、反復可能にしたい場合、ジェネレータを使用します。
単純にするために、いくつかの重要な点を省略すると、ジェネレータは “値を生成(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]() { // [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++) {
// 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 (間に遅延をはさみながら)
}
})();