2024年7月23日

非同期イテレーションとジェネレータ

非同期イテレーションを使用すると、要求に応じて非同期に来るデータに対して反復処理することができます。例えば、ネットワーク経由でチャンクごとに何かをダウンロードする場合です。そして、非同期ジェネレータはそれをさらに便利にします。

最初に単純な例を見て構文を把握し、その後、実際のユースケースを見ていきましょう。

反復可能(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 や他の種類の遅延処理の後)に値がくるときに必要になります。

一般的なケースは、オブジェクトが次の値を提供するためにネットワークリクエストを行う必要がある場合です。実際の例については、少し後で説明します。

オブジェクトを非同期的に反復可能とするには、:

  1. Symbol.iterator の代わりに、Symbol.asyncIterator を使用します。
  2. next() は promise を返す必要があります(次の値で解決される)。
    • async キーワードはこれを処理します。シンプルに async next() とできます。
  3. このようなオブジェクトをイテレートするには、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
  }

})()

ご覧の通り、構成は通常のイテレータと同様です:

  1. オブジェクトを非同期的に反復可能にするために、Symbol.asyncIterator メソッドが必要です。 (1)
  2. それは promise を返す next() メソッドを持つオブジェクトを返す必要があります。(2)
  3. next() メソッドは async である必要はなく、promise を返す通常のメソッドかもしれませんが、async は中では await が使えるので便利です。ここでは単に1秒だけ遅延させています。(3)
  4. イテレートするために、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 (間に遅延をはさみながら)
  }

})();