- A quick intro
- JavaScript におけるジェネレーター関数
- ジェネレーター関数とは?
- Your first generator function
- イタラブルとイテレータ
- Iterables are spreadable and destructurable
- ジェネレーター関数に話を戻すと、
- ジェネレーター関数の使用例
- JavaScriptにおける非同期ジェネレータ関数
- 非同期ジェネレータ関数とは
- 最初の非同期ジェネレータ関数
- 非同期ジェネレーターからデータを抽出する
- Talking back to asynchronous generator functions
- 非同期反復処理と非同期ジェネレータ関数の使用例
- Wrapping up
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);}
その他に、Map
や Set
も反復可能として知られています。 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()
は、プロパティが value
と done
である別のオブジェクトを返す必要があります。
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
オブジェクトは body
を getReader()
を持つ読み取り可能なストリームとして公開します。 このようなストリームを非同期ジェネレーターでラップし、後で 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.
これらは代わりに非同期ジェネレーター関数のビルディング ブロックです。
イテラブルとイテレータをよく理解すると、長い道のりを歩むことになります。 ジェネレーター関数と非同期ジェネレーター関数を毎日使用するわけではありませんが、ツールベルトの中に入れておくと便利なスキルです。 使ったことはありますか?
読んでくれてありがとう、このブログにご期待ください!
。