Node.js の非同期ジェネレータと非同期反復の詳細な説明

Node.js の非同期ジェネレータと非同期反復の詳細な説明

序文

ジェネレーター関数は、async/await が導入される前から JavaScript に存在していました。つまり、非同期ジェネレーター (常に Promise を返し、待機できるジェネレーター) を作成するときに注意すべき点が多数あります。

今日は、非同期ジェネレーターと、それに近い非同期反復処理について見ていきます。

注: これらの概念はすべての最新の JavaScript 実装に適用されるはずですが、この記事のすべてのコードは Node.js バージョン 10、12、および 14 に対して開発およびテストされています。

非同期ジェネレータ関数

この小さなプログラムを見てみましょう:

// ファイル: main.js
const createGenerator = 関数*(){
 'a' を得る
 'b' を得る
 'c' を得る
}

定数main = () => {
 定数ジェネレータ = createGenerator()
 for (ジェネレータのconst項目) {
 コンソール.log(アイテム)
 }
}
主要()

このコードはジェネレーター関数を定義し、その関数を使用してジェネレーター オブジェクトを作成し、次に for ... of ループを使用してジェネレーター オブジェクトを反復処理します。かなり標準的なものですが、現実世界ではこれほど些細なことにジェネレータを使用することはありません。ジェネレーターと for ... of ループに慣れていない場合は、「Javascript ジェネレーター」と「ES6 ループと反復可能オブジェクト」の記事を参照してください。非同期ジェネレータを使用する前に、ジェネレータと for...of ループについてしっかりと理解しておく必要があります。

ジェネレーター関数で await を使用したいとします。Node.js は、関数を async キーワードで宣言する限り、この機能をサポートします。非同期関数に慣れていない場合は、「Writing Asynchronous Tasks in Modern JavaScript」の記事をご覧ください。

プログラムを修正して、ジェネレーターで await を使用しましょう。

// ファイル: main.js
const createGenerator = 非同期関数*(){
 新しい Promise((r) => r('a')) を待機します。
 'b' を得る
 'c' を得る
}

定数main = () => {
 定数ジェネレータ = createGenerator()
 for (ジェネレータのconst項目) {
 コンソール.log(アイテム)
 }
}
主要()

また、現実の世界では、このようなことは行いません。おそらく、サードパーティの API またはライブラリからの関数を待機することになるでしょう。誰もが理解しやすいように、例はできる限りシンプルにしています。

上記のプログラムを実行しようとすると、次の問題が発生します。

$ ノードメイン.js
/Users/alanstorm/Desktop/main.js:9
 for (ジェネレータのconst項目) {
 ^
TypeError: ジェネレータは反復可能ではありません

JavaScript によれば、このジェネレータは「反復可能ではない」とのことです。一見すると、ジェネレーター関数を非同期にすることは、それが生成するジェネレーターが反復可能ではないことも意味しているように見えるかもしれません。ジェネレーターの目的は「プログラム的に」反復可能なオブジェクトを生成することなので、これは少し混乱を招きます。

次に、何が起こったのかを把握します。

発電機をチェックする

JavaScriptジェネレータの反復可能オブジェクト[1]を見てみましょう。オブジェクトに next メソッドがある場合、そのオブジェクトはイテレータ プロトコルを実装します。また、 next メソッドは、value プロパティ、done プロパティ、または value プロパティと done プロパティの両方を持つオブジェクトを返します。

次のコードを使用して、非同期ジェネレータ関数によって返されるジェネレータ オブジェクトと通常のジェネレータ関数によって返されるジェネレータ オブジェクトを比較します。

// ファイル: test-program.js
const createGenerator = 関数*(){
 'a' を得る
 'b' を得る
 'c' を得る
}

const createAsyncGenerator = 非同期関数*(){
 新しい Promise((r) => r('a')) を待機します。
 'b' を得る
 'c' を得る
}

定数main = () => {
 定数ジェネレータ = createGenerator()
 定数 asyncGenerator = createAsyncGenerator()

 console.log('ジェネレータ:',ジェネレータ[シンボル.イテレータ])
 console.log('asyncGenerator',asyncGenerator[Symbol.iterator])
}
主要()

前者には Symbol.iterator メソッドがありませんが、後者にはあることがわかります。

$ ノードテストプログラム.js
ジェネレータ: [関数: [シンボル.イテレータ]]
asyncGenerator 未定義

両方のジェネレーター オブジェクトには next メソッドがあります。次のメソッドを呼び出すようにテスト コードを変更すると、次のようになります。

// ファイル: test-program.js

/* ... */

定数main = () => {
 定数ジェネレータ = createGenerator()
 定数 asyncGenerator = createAsyncGenerator()

 console.log('ジェネレータ:',generator.next())
 コンソールにログ出力します。
}
主要()

別の問題も発生します:

$ ノードテストプログラム.js
ジェネレータ: { 値: 'a'、完了: false }
asyncGenerator Promise { <保留中> }

オブジェクトを反復可能にするには、 next メソッドが value プロパティと done プロパティを持つオブジェクトを返す必要があります。非同期関数は常に Promise オブジェクトを返します。この機能は、非同期関数で作成されたジェネレーターに適用されます。これらの非同期ジェネレーターは常に Promise オブジェクトを生成します。

この動作により、非同期ジェネレーターは JavaScript 反復プロトコルを実装できなくなります。

非同期反復

幸いなことに、この矛盾を解決する方法があります。非同期ジェネレータによって返されるコンストラクタまたはクラスを見ると

// ファイル: test-program.js
/* ... */
定数main = () => {
 定数ジェネレータ = createGenerator()
 定数 asyncGenerator = createAsyncGenerator()

 コンソールログ('asyncGenerator',asyncGenerator)
}

これは、Generator ではなく AsyncGenerator の型、クラス、またはコンストラクターを持つオブジェクトであることがわかります。

asyncGenerator オブジェクト [AsyncGenerator] {}

このオブジェクトは反復可能ではないかもしれませんが、非同期的に反復可能です。

オブジェクトを非同期的に反復可能にするには、Symbol.asyncIterator メソッドを実装する必要があります。このメソッドは、反復子プロトコルの非同期バージョンを実装するオブジェクトを返す必要があります。つまり、オブジェクトには Promise を返す next メソッドが必要であり、その Promise は最終的に done プロパティと value プロパティを持つオブジェクトに解決される必要があります。

AsyncGenerator オブジェクトはこれらすべての条件を満たします。

これで疑問が残ります - 反復可能ではないが非同期的に反復できるオブジェクトをどのように反復できるでしょうか?

for await … ループの

非同期反復可能オブジェクトは、ジェネレーターの next メソッドのみを使用して手動で反復できます。 (ここでのメイン関数は async main になっていることに注意してください。これにより、関数内で await を使用できるようになります)

// ファイル: main.js
const createAsyncGenerator = 非同期関数*(){
 新しい Promise((r) => r('a')) を待機します。
 'b' を得る
 'c' を得る
}

定数main = 非同期() => {
 定数 asyncGenerator = createAsyncGenerator()

 結果 = { done:false } とします
 while(!result.done) {
 結果 = asyncGenerator.next() を待機します
 if(result.done) { 続行; }
 console.log(結果.値)
 }
}
主要()

ただし、これは最も単純なループ メカニズムではありません。 while ループ条件は気に入らないし、 result.done を手動でチェックするのも嫌です。さらに、 result.done 変数は、内部ブロックと外部ブロックの両方のスコープ内に存在する必要があります。

幸いなことに、非同期イテレータをサポートするほとんどの (おそらくすべての?) JavaScript 実装は、特殊な for await ... of ループ構文もサポートしています。例えば:

const createAsyncGenerator = 非同期関数*(){
 新しい Promise((r) => r('a')) を待機します。
 'b' を得る
 'c' を得る
}

定数main = 非同期() => {
 定数 asyncGenerator = createAsyncGenerator()
 asyncGeneratorのconst項目をawaitします。
 コンソール.log(アイテム)
 }
}
主要()

上記のコードを実行すると、非同期ジェネレーターと反復可能オブジェクトが正常にループされ、ループ本体で Promise の完全に解決された値が取得されることがわかります。

$ ノードメイン.js
1つの
b
c

for await ... of ループでは、非同期反復子プロトコルを実装するオブジェクトが優先されます。しかし、これを使用してあらゆる種類の反復可能なオブジェクトを反復処理することができます。

await(const [1,2,3] の項目) {
 コンソール.log(アイテム)
}

for await を使用すると、Node.js はまずオブジェクト上で Symbol.asyncIterator メソッドを検索します。見つからない場合は、Symbol.iterator メソッドを使用します。

非線形コード実行

await と同様に、for await ループはプログラムに非線形コード実行を導入します。つまり、コードは記述された順序とは異なる順序で実行されます。

プログラムが最初に for await ループに遭遇すると、オブジェクトに対して next を呼び出します。

オブジェクトは promise を生成し、コードの実行により非同期関数が終了し、プログラムの実行はその関数の外部で継続されます。

プロミスが解決されると、コード実行はこの値でループ本体に戻ります。

ループが終了し、次のトリップに進むとき、Node.js はオブジェクトに対して next を呼び出します。この呼び出しにより別の promise が生成され、コード実行によって再び関数が終了します。このパターンは、Promise が done が true であるオブジェクトに解決されるまで繰り返され、その後、for await ループの後のコードの実行が続行されます。

次の例はこの点を示しています。

カウントを 0 にする
定数getCount = () => {
 カウント++
 `${count}.` を返します。
}

const createAsyncGenerator = 非同期関数*() {
 console.log(getCount() + 'createAsyncGenerator の入力')

 console.log(getCount() + '出力しようとしています')
 新しい Promise((r)=>r('a')) を待機します

 console.log(getCount() + 'createAsyncGenerator を再入力しています')
 console.log(getCount() + 'b を生成しようとしています')
 'b' を得る

 console.log(getCount() + 'createAsyncGenerator を再入力しています')
 console.log(getCount() + 'c を生成しようとしています')
 'c' を得る

 console.log(getCount() + 'createAsyncGenerator を再入力しています')
 console.log(getCount() + 'createAsyncGenerator を終了しています')
}

定数main = 非同期() => {
 console.log(getCount() + 'メインに入る')

 定数 asyncGenerator = createAsyncGenerator()
 console.log(getCount() + 'for await ループを開始しています')
 asyncGeneratorのconst項目をawaitします。
 console.log(getCount() + 'for await ループに入ります')
 console.log(getCount() + アイテム)
 console.log(getCount() + 'for await ループを終了しています')
 }
 console.log(getCount() + 'for await ループが完了しました')
 console.log(getCount() + 'メインを離れます')
}

console.log(getCount() + 'main を呼び出す前')
主要()
console.log(getCount() + 'main を呼び出した後')

このコードでは、実行を追跡できる番号付きのログ記録ステートメントを使用します。練習として、自分でプログラムを実行して結果を確認する必要があります。

非同期反復は強力な手法ですが、その仕組みを理解していないとプログラムの実行時に混乱が生じる可能性があります。

要約する

Node.js の非同期ジェネレーターと非同期反復に関するこの記事はこれで終わりです。Node.js の非同期ジェネレーターと非同期反復に関するより関連性の高いコンテンツについては、123WORDPRESS.COM で以前の記事を検索するか、次の関連記事を引き続き参照してください。今後も 123WORDPRESS.COM を応援していただければ幸いです。

以下もご興味があるかもしれません:
  • シングルスレッドJavaScriptにおける非同期処理実装の詳細な説明
  • JSシングルスレッド非同期IOコールバックの特性を分析する
  • Javascript 非同期プログラミング: Promise を本当に理解していますか?
  • JavaScript 非同期プログラミングにおける Promise の初期の使用法の詳細な説明
  • JS 非同期実行の原則とコールバックの詳細
  • 最新の JavaScript で非同期タスクを書く方法
  • 1 つの記事で Node.js の非同期プログラミングを学ぶ
  • Node.js における非同期プログラミングの知識ポイントの詳細な説明
  • JS の 3 つの主要な問題、非同期性とシングルスレッドについて簡単に説明します。

<<:  Linux で Xfce デスクトップ環境を使用すべき 8 つの理由

>>:  SQL における distinct と row_number() over() の違いと使い方

推薦する

MySQL OOM (メモリオーバーフロー) の解決策

OOM は「Out Of Memory」の略で、メモリオーバーフローを意味します。メモリ オーバーフ...

DockerはRedis5.0をビルドし、データをマウントします

目次1. 永続データの簡単なマウント2. DockerFileでイメージをビルドし、設定ファイルを指...

CSS の読み込みによってブロックが発生しますか?

おそらく誰もが js の実行によって DOM ツリーの解析とレンダリングがブロックされることを知って...

CSS グリッドレイアウトで列にアイテムを埋め込む方法

n 個のアイテムがあり、これらのアイテムをグリッド レイアウトの列に並べ替える必要があるとします。列...

コード例を通してページ置換アルゴリズムの原理を理解する

ページ置換アルゴリズム: 本質は、限られたメモリをワイヤレス プロセスに対応できるようにすることです...

Ubuntu 20.04 と NVIDIA ドライバーのインストールに関するチュートリアル

Ubuntu 20.04をインストールする NVIDIAドライバーをインストールする Pytouch...

Vue3はCSSの無限シームレススクロール効果を実装します

この記事では、CSS無限シームレススクロール効果を実現するためのvue3の具体的なコードを参考までに...

Webページ作成の質問: 画像ファイルのパス

この記事は 123WORDPRESS.COM Lightning によるオリジナルです。転載する際に...

CentOS8.0 で FTP サーバーをインストールして設定する方法

CentOS8.0-1905 のリリース後、FTP サーバーを CentOS の新しいバージョンに移...

フロントエンドJavaScript ES6の詳細について

目次1. はじめに1.1 Babel トランスコーダ1.2 ポリフィル2. let と const ...

入力要素 [type="file"] を使用する場合のスタイルのカスタマイズとブラウザの互換性の問題に関する議論

この2日間、Baixing.comの筆記試験問題を解いているときに、このような問題に遭遇しました。H...

Linux環境変数の設定に関する完全なガイド

Linux環境変数の設定ソフトウェアのインストールをカスタマイズする場合、多くの場合、環境変数を設定...

CSS3を使用してテキストの垂直配置を実現する方法

最近のプロジェクトでは、テキストを垂直に揃えたいと考え、CSS の writing-mode プロパ...

Git サーバーを使用してデバッグ ブランチを表示し、修正する方法を 1 日 1 分で学習します。

デバッグブランチプロジェクトの通常の開発中に、以前にリリースされたバージョンにバグがある場合がありま...

Docker での Redis 接続の急増をトラブルシューティングした実践的な記録

土曜日、本番サーバー上の Redis サーバーが利用できなくなり、エラー メッセージは次のようになり...