A quick intro

Welcome to the wonderful world of generator functions and asynchronous generators in JavaScript.

You are about learning about one of most exotic (according to the majority of developers) JavaScript feature.これは、JavaScript が使用する最もエキサイティングな機能です(開発者の大多数による)。

さっそく始めましょう!

JavaScript におけるジェネレーター関数

ジェネレーター関数とは?

JavaScript におけるジェネレーター関数 (ECMAScript 2015) は同期関数の特殊なタイプで、任意で実行を停止および再開することができるものです。

Fire and Forget である通常の JavaScript 関数とは対照的に、ジェネレーター関数は次のような機能も備えています:

  • 双方向チャネルを介して呼び出し側と通信します。

ジェネレーター関数はステロイドのクロージャと考えることができますが、類似点はここまでです!

Your first generator function

ジェネレーター関数を作成するには、function キーワードの後に星印 * を付けます:

function* generate() {//}

注:ジェネレーター関数はクラスメソッドの形や関数式も取ることができます。

一度関数の内部で、実行を一時停止するために yield を使用することができます。

function* generate() { yield 33; yield 99;}

yield実行を一時停止して呼び出し元に Generator オブジェクトと呼ばれるものを返すのです。

これらの概念を解明しましょう。

イタラブルとイテレータ

JavaScript のイタラブル オブジェクトは Symbol.iterator を実装したオブジェクトです。 以下は、イテラブルの最小限の例です。

const iterable = { : function() { /* TODO: iterator */ }};

このイテラブル オブジェクトができたら、 に関数を割り当ててイテレータ オブジェクトを返します。

これは多くの理論のように思えますが、実際にはイテラブルは for...of でループ処理できるオブジェクトなのです (ECMAScript 2015)。 たとえば、配列や文字列は反復可能です。

for (const char of "hello") { console.log(char);}

その他に、MapSet も反復可能として知られています。 for...of はオブジェクトの値に対する反復処理に便利です:

const person = { name: "Juliana", surname: "Crain", age: 32};for (const value of Object.values(person)) { console.log(value);}

ただ、enumerable: false としてマークされたプロパティは反復処理に表示されないことを忘れないでください:

const person = { name: "Juliana", surname: "Crain", age: 32};Object.defineProperty(person, "city", { enumerable: false, value: "London"});for (const value of Object.values(person)) { console.log(value);}// Juliana// Crain// 32

今、カスタム反復処理の問題は反復処理なしでは遠くに進めないということです。

イテレータもオブジェクトですが、イテレータ プロトコルに準拠する必要があります。 簡単に言うと、イテレータは少なくとも next() メソッドを持たなければなりません。

next() は、プロパティが valuedone である別のオブジェクトを返す必要があります。

next()メソッドのロジックは次のルールに従わなければならない。

  • 反復を続けるためにdone: falseでオブジェクトを返す。

value の代わりに、consumer に生成したい結果を保持します。

Iterator を追加して、この例を拡張してみましょう。 4830>

  • count が 3 になるまで { value: x, done: false} の形をしたオブジェクト。
  • count が 3 になると { value: x, done: true} の形をしたオブジェクト。

この最小反復記号は for...of で反復する準備ができています:

const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};for (const iterableElement of iterable) { console.log(iterableElement);}

その結果は:

123

Generator 関数をよく理解すれば、反復記号が generator オブジェクトの基本であることがわかってくるはずです。

  • イテレート可能なオブジェクトは、反復処理を行うためのものです。
  • イテレーターは、反復可能な… “ループ可能” にするものです。

結局、反復可能のポイントは何なのでしょうか。

私たちは今、JavaScript でカスタムまたはネイティブのほぼすべてのデータ構造に対して事実上動作する、標準の信頼できる for...of ループを持ちます。

カスタム データ構造で for...of を使用するには、次のようにします:

  • Symbol.iterator の実装.
  • イテレーター オブジェクトを提供.

以上!

さらなるリソース:

  • Jake Archibald による Iterators gonna iterate.

Iterables are spreadable and destructurable

for...of に加えて、有限の iterable に対して spread と destructuring を使用することもできます。

前の例をもう一度考えてみましょう。

const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};

すべての値を引き出すには、反復処理対象を配列に広げます。

const values = ;console.log(values); // 

代わりにいくつかの値のみを引き出すには、反復処理対象の配列を非構造化します。

const = iterable;console.log(first); // 1console.log(second); // 2

ここで、代わりに 1 番目と 3 番目の値を取得します。

const = iterable;console.log(first); // 1console.log(third); // 3

ここで、ジェネレーター関数に再び焦点を当てましょう。

ジェネレーターから値を引き出すには、次の 3 つのアプローチを使用できます:

  • iterator オブジェクトで next() を呼び出す。
  • for...of による反復処理、
  • による拡散、および配列の再構築。

ジェネレータ関数は、通常の関数のように 1 つのステップでそのすべての結果を計算するわけではありません。

私たちの例では、ジェネレーターから値を取得するために、まず最初にジェネレーターをウォームアップします:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();

ここで go は反復可能オブジェクト/反復記号になり、generate の呼び出し結果である。

Generatorオブジェクトは反復可能かつ反復子であることに注意してください)。

これから、実行を進めるために go.next() を呼び出すことができます:

function* generate() { yield 33; yield 99;}const go = generate();// Consume the generatorconst { value: firstStep } = go.next(); // firstStep is 33const { value: secondStep } = go.next(); // secondStep is 99

ここで go.next() への呼び出しごとに新しいオブジェクトが作られる。 この例では、このオブジェクトからプロパティ value をデストラクチャします。

イテレータオブジェクトで next() を呼び出して返されたオブジェクトは、2つのプロパティを持っています:

  • value: 現在のステップに対する値
  • done: ジェネレータにさらに値があるかどうかを示すブーリ アン

前のセクションで、こうしたイタレータオブを実装してきました。

next() はイテレータオブジェクトから有限のデータを取り出すのに有効です。

有限でないデータを反復するためには、for...of を使います。 以下はエンドレス ジェネレータです:

function* endlessGenerator() { let counter = 0; while (true) { counter++; yield counter; }}// Consume the generatorfor (const value of endlessGenerator()) { console.log(value);}

for...of を使うとき、ジェネレータを初期化する必要がないことがわかるでしょう。

最後に、ジェネレータ オブジェクトを拡散したりデストラクションすることも可能です。 以下はスプレッドです:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Spreadconst values = ;console.log(values); // 

以下はデストラクションです:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Destructuringconst = go;console.log(first); // 1console.log(second); // 2

注意すべき点は、すべての値を消費するとジェネレータは排気される点です。

ジェネレーターから値を展開すると、その後に引き出すものは何も残りません:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Spreadconst values = ;console.log(values); // // Exhaustconst = go;console.log(first); // undefinedconsole.log(second); // undefined

ジェネレーターは逆方向にも働きます。

ジェネレーター関数に話を戻すと、

ジェネレーター関数呼び出しの結果オブジェクトであるイテレーター オブジェクトは、次のメソッドを公開しています。

その用途はデータを取り出すだけでなく、ジェネレータに値を送ることもできます。

次のジェネレータを考えてみましょう。 初期化時にこの引数を与える。

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");

イテレータオブジェクトで初めてnext()を呼ぶとすぐに実行が始まり、「A」が生成される。

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");console.log(go.next().value); // A

この時点で、next に引数を与えることにより、ジェネレータに話を戻すことができます:

go.next("b");

以下はその完全版です。

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");console.log(go.next().value); // Aconsole.log(go.next("b").value); // B

これから、新しい大文字の文字列が必要なときはいつでも、yield に値をフィードすることが可能です。

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");console.log(go.next().value); // Aconsole.log(go.next("b").value); // Bconsole.log(go.next("c").value); // Cconsole.log(go.next("d").value); // D

いつでも実行から完全に戻りたい場合は、イテレータオブジェクトで return を使用します:

const { value } = go.return("stop it");console.log(value); // stop it

これで実行が停止されました。

値に加えて、ジェネレータに例外を投げることもできます。 ジェネレーター関数のエラー処理」を参照してください。

ジェネレーター関数の使用例

ほとんどの開発者 (私も含めて) は、実世界ではほとんどあるいはまったく適用されないエキゾチックな JavaScript 機能としてジェネレーター関数を捉えています。

これは、jQuery と少しの CSS でほとんどの場合うまくいく、平均的なフロントエンド作業に当てはまるかもしれません。

実際には、パフォーマンスが最優先されるすべてのシナリオでジェネレーター関数が真に輝きます。

  • バックエンドまたはフロントエンドでデータを扱う。
  • データの無限シーケンスを生成する。
  • オンデマンドで高価なロジックを計算する。

    JavaScriptにおける非同期ジェネレータ関数

    非同期ジェネレータ関数とは

    非同期ジェネレータ関数(ECMAScript 2018)は、任意で実行を停止および再開できる非同期関数の特殊なタイプである。

    同期ジェネレーター関数と非同期ジェネレーター関数の違いは、後者がイテレーター オブジェクトから非同期の Promise ベースの結果を返すことです。

    ジェネレーター関数と同様に、非同期ジェネレーター関数は、次のことを実行することができます。

  • 最初の非同期ジェネレータ関数

    非同期ジェネレータ関数を作成するには、*という星印とasyncというプレフィックスを付けてジェネレータ関数を宣言します。

    async function* asyncGenerator() { //}

    一度関数内で、yieldを使って実行を一時停止することができます:

    async function* asyncGenerator() { yield 33; yield 99;}

    ここでyieldは実行を停止して呼び出し元にいわゆるGeneratorオブジェクトを返すというもの。

    このオブジェクトは反復可能であり、同時に反復子でもあります。

    これらの概念が非同期の土地でどのように適合するかを確認するために復習しましょう。

    以下に最小限の例を示します。

    const asyncIterable = { : function() { /* TODO: iterator */ }};

    この反復可能オブジェクトができたら、 に関数を割り当て、反復可能オブジェクトを返します。

    イテレータ オブジェクトは、(同期イテレータのように) next() メソッドを持つイテレータ プロトコルに準拠する必要があります。

    イテレータを追加して例を展開してみましょう:

    const asyncIterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return Promise.resolve({ value: count, done: false }); } return Promise.resolve({ value: count, done: true }); } }; }};

    このイテレータは前のセクションで構築したものと同様で、今回の唯一の違いは返すオブジェクトを Promise.resolve でラップする点です。

    この時点で、次のようなことができます:

    const go = asyncIterable();go.next().then(iterator => console.log(iterator.value));go.next().then(iterator => console.log(iterator.value));// 1// 2

    または for await...of で:

    async function consumer() { for await (const asyncIterableElement of asyncIterable) { console.log(asyncIterableElement); }}consumer();// 1// 2// 3

    非同期の反復処理および反復処理機能は非同期の生成関数の基礎となるものです。

    では、それらに再び焦点を当ててみましょう。

    非同期ジェネレーターからデータを抽出する

    非同期ジェネレーター関数は、通常の関数のようにすべての結果を 1 つのステップで計算するわけではありません。

    非同期イテレーターおよびイテレート可能ファイルを調べた後、非同期ジェネレーターから Promise を引き出すために、次の 2 つのアプローチを使用できることは驚くことではありません:

    • Iterator オブジェクトで next() を呼び出す。
    • for await...of による非同期反復。

    最初の例では、次のようになります:

    async function* asyncGenerator() { yield 33; yield 99;}const go = asyncGenerator();go.next().then(iterator => console.log(iterator.value));go.next().then(iterator => console.log(iterator.value));

    このコードからの出力は次のとおりです:

    3399

    もう一方のアプローチは for await...of による非同期の反復を使用しています。 非同期反復を使用するには、コンシューマーを async 関数でラップします。

    以下は完全な例です。

    Talking back to asynchronous generator functions

    Consider the following asynchronous generator:

    async function* asyncGenerator(string) { while (true) { string = yield string.toUpperCase(); }}

    Much like the endless uppercase machine from the generator example, we can provide an argument to next():

    async function* asyncEndlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}async function consumer() { const go = await asyncEndlessUppercase("a"); const { value: firstStep } = await go.next(); console.log(firstStep); const { value: secondStep } = await go.next("b"); console.log(secondStep); const { value: thirdStep } = await go.next("c"); console.log(thirdStep);}consumer();

    here each step sends a new value into the generator.と、この例では非同期ジェネレーターに新しい値を送信します。

    このコードの出力は次のとおりです。

    ABC

    たとえ toUpperCase() に本質的に非同期のものがなくても、このパターンの目的を感じ取ることができます。

    いつでも実行から抜け出したい場合は、イテレータ オブジェクトに対して return() を呼び出すことができます:

     const { value } = await go.return("stop it"); console.log(value); // stop it

    値に加えて、ジェネレータに例外をスローすることも可能です。 非同期ジェネレータのエラー処理」を参照してください。

    非同期反復処理と非同期ジェネレータ関数の使用例

    ジェネレータ関数が大きなファイルや無限シーケンスで同期的に動作するのに適しているとすれば、非同期ジェネレータ関数は JavaScript で可能性のまったく新しい土地を可能にするものです。

    特に、非同期反復処理により、読み取り可能なストリームを簡単に消費できるようになります。 Fetch の Response オブジェクトは bodygetReader() を持つ読み取り可能なストリームとして公開します。 このようなストリームを非同期ジェネレーターでラップし、後で for await...of で反復処理できます。

    Async iterators and generators by Jake Archibald には、たくさんの素晴らしい例が掲載されています。

    ストリームの他の例としては、Fetch によるリクエスト ストリームがあります。

    執筆時点では Symbol.asyncIterator を実装したブラウザ API はありませんが、Stream 仕様がこれを変えようとしています。

    将来的には、クライアント側で書き込み可能、読み取り可能、および変換可能なストリームを消費してシームレスに動作できるようになる予定です。

    Further resources:

    • The Stream API.

    Wrapping up

    Key terms and concepts we covered in this post:

    ECMAScript 2015:

    • iterable.The Stream API.The Stream API.The Stream APIs.Iterable.Iterable.
    • iterator.

    これらはジェネレーター関数の構成要素です。

    ECMAScript 2018:

    • asynchronous iterable.
    • asynchronous iterator.

    これらは代わりに非同期ジェネレーター関数のビルディング ブロックです。

    イテラブルとイテレータをよく理解すると、長い道のりを歩むことになります。 ジェネレーター関数と非同期ジェネレーター関数を毎日使用するわけではありませんが、ツールベルトの中に入れておくと便利なスキルです。 使ったことはありますか?

    読んでくれてありがとう、このブログにご期待ください!

    コメントを残す

    メールアドレスが公開されることはありません。