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

推薦する

HTML 左、中央、右の適応レイアウト (calc css 式を使用)

最新の HTML 標準には、レイアウトを計算するために使用できる calc CSS 式があります。し...

例を通してMySQLの更新がテーブルをロックするかどうかを判定する

2つのケース: 1. 索引あり 2. 索引なし前提条件:方法: コマンドラインを使用してシミュレート...

WeChatアプレットを使用して天井効果を実現する方法の例

目次1. 実装2. 問題点3. より良い実装方法があるかどうか検討する要約する背景は日付のタイトルで...

Mysql 自己結合クエリ例の詳細な説明

この記事では、Mysql の自己結合クエリについて説明します。ご参考までに、詳細は以下の通りです。自...

JavaScript で簡単なモグラ叩きゲームを実装する

この記事では、モグラ叩きゲームを実装するためのJavaScriptの具体的なコードを参考までに紹介し...

vue3 コンポーネント通信方法の概要と例

vue3コンポーネントの通信モードは次のとおりです。小道具$放出$expose / 参照$属性vモデ...

MySQL truncate table ステートメントの使用

Truncate table ステートメントは、テーブル内のすべてのデータを削除/切り捨てるために使...

MySQL query_cache_type パラメータと使用方法の詳細

MySQL クエリ キャッシュを設定する目的は次のとおりです。クエリ結果をキャッシュしておくと、次回...

Websocket+Vuexはリアルタイムチャットソフトウェアを実装します

目次序文1. 効果は図の通りです2. 具体的な実施手順1. Vuexの紹介2.webscoked実装...

ウェブページの HTML コード: スクロールテキストの作成

このセクションでは、Web ページ内のテキストをスクロールしたり、スクロール プロパティを制御できる...

Nginx ソースコードのコンパイルとインストールのプロセス記録

rpm パッケージのインストールは比較的簡単なので、ここでは説明しません。ほとんどのオープンソース ...

HTML の 2 つのタブ ナビゲーション間の競合の解決方法

まず問題の説明から始めましょう:同じページで、1 つのタブに float:left が必要で、もう ...

CSS3 画像の境界線を学ぶのに役立つ記事

CSS3 border-image プロパティを使用すると、要素の周囲に画像の境界線を設定できます。...

ボタンを使用してフォームを送信する代わりに、画像を使用してフォームを送信します。

コードをコピーコードは次のとおりです。 <フォームメソッド="post" ...

インターネット接続なしでLinux Centos7にアプリケーションをインストールする方法の詳細な説明

1. 前の章では、プログラムを yum リポジトリに直接インストールできることを学びましたが、そのた...