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

推薦する

Nginx リバース プロキシ学習例チュートリアル

目次1. リバースプロキシの準備1. LinuxシステムにTomcatをインストールする2. Tom...

CentOS 7 での mysql 5.7 のインストール チュートリアル

1. 公式MySQL Yumリポジトリをダウンロードしてインストールする 実行ファイル: mysql...

トークン生成と検証を実装するミニプログラム

目次プロセスデモミニプログラムバックエンドインターフェースプロセス各リクエストインターフェースは検証...

ReactのuseEffectクロージャの落とし穴についての簡単な説明

問題コードuseEffectによって発生したクロージャの問題コードを見てみましょう 定数 btn =...

Dockerコンテナの個別展開のためのLNMPの実装

1. 環境整備各コンテナの IP アドレス: nginx: 172.16.10.10マイSQL: 1...

MySQL では SQL ステートメントはどのように実行されますか?

目次1. MySQLアーキテクチャの分析1.1 コネクタ1.2 クエリキャッシュ1.3 アナライザー...

MySQL で遅い SQL 文を見つける方法

MySQL で遅い SQL ステートメントを見つけるにはどうすればよいでしょうか?これは、多くの人を...

HTMLの最適化によりWebページの速度が向上

明らかな HTML、隠された「公開スクリプト」 Web ページのダウンロード時間を短縮する鍵は、フ...

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

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

Dockerコンテナでyumを呼び出すときのエラーの解決方法

dockerfile またはコンテナ内で yum を実行すると、エラーが報告され、ソースが見つかりま...

HTML ヘッド構造

以下では、よく使われるヘッド構造と、各タグや要素の意味や使用シーンを紹介します(この記事は、Yisi...

JavaScript でサウンド効果付きの花火効果を実装する

コードを書くのに 30 分かかりましたが、この HTML5 Canvas New Year Fire...

Javascriptの基本ループの詳しい説明

目次サイクルのために入室のためのその間しながら行うループから抜け出す要約するサイクルのためにループは...

ウェブデザインにおけるテキスト入力ボックスのパラメータの説明

<br />一般的なゲストブック、フォーラムなどでは、テキスト入力ボックスが使われていま...

CSS 動的高さ遷移アニメーション効果の実装

この質問は、Nuggets のメッセージから生まれました。友人が、次のコードの高さ遷移アニメーション...