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() の違いと使い方

推薦する

Dockerコンテナ内のホストのホスト名が取得できない問題の解決方法

Node.js環境でテストが通っています。他の言語でも同様です。環境変数を取得する方法を使うだけです...

初心者向け入門チュートリアル④:サブディレクトリのバインド方法

これが何を意味するのかを理解するには、まずサブディレクトリとは何かを知る必要があります。では、サブデ...

Linuxファイルコマンドの使用

1. コマンドの紹介ファイル コマンドは、ファイルの種類を識別するために使用されます。ファイル チェ...

nginx をベースにリロードなしでアップストリーム サーバーの動的な自動起動と停止を実装する方法

目次1. Consulクラスタをデプロイする1. 準備3. Consulクラスタを作成する4. 管理...

JD.com フラッシュセール効果を実現する JavaScript

この記事では、JD.comのフラッシュセール効果を実現するためのJavaScriptの具体的なコード...

XHTML と CSS の Web ページ作成の問題に対する解決策

XHTML CSS ページ制作中に遭遇する問題の解決策は、解決策と呼ぶには少々大げさです。せいぜい、...

MySql 範囲内の検索時にインデックスが有効にならない理由の分析

1 問題の説明この記事では、確立された複合インデックスをソートし、レコード内の非インデックス フィー...

MySQL5.7 並列レプリケーションの原理と実装

データ操作とメンテナンスに少しでも知識のある人なら、MySQL 5.5 以前では再生に単一の SQL...

TCPパフォーマンスチューニングの実装原理とプロセス分析

3ウェイハンドシェイクフェーズクライアントSYNパケットの再試行回数sysctl -w net.ip...

nginx で HSTS を有効にしてブラウザを HTTPS アクセスにリダイレクトする方法の詳細な説明

前回の記事では、https を使用したローカルノードサービスアクセスを実装しました。前回の記事の効果...

MySQL 8.0 でリモートアクセス権限を設定する方法

前回の記事では、MySQL パスワードをリセットする方法を説明しました。一部の学生から、データベース...

MySQLデータのバックアップとリカバリの実装方法の分析

この記事では、例を使用して MySQL データのバックアップと復元の方法について説明します。ご参考ま...

Mac で MySQL バージョン 5.6 のパスワードを設定する方法

MySQLはインストール時に設定できますが、それより低いバージョンは設定できないようで、インストール...

大きなオフセットによる MySQL 制限ページングが遅い理由と最適化ソリューション

MySQL では通常、limit を使用してページ上のページング機能を完了しますが、データ量が大きな...