この記事では、Node.js のイベント ループのワークフローとライフ サイクルについて詳しく説明します。必要な方は参考にしていただければ幸いです。
この記事では、node.js イベント ループのワークフローとライフ サイクルについて詳しく説明します
最も一般的な誤解の 1 つは、イベント ループはJavascript エンジンの一部 (V8、spiderMonkey など)。実際、イベント ループは主に Javascript エンジンを使用してコードを実行します。
まず第一に、スタックはありません。第二に、このプロセスは複数のキュー (データ構造内のキューのような) が関与するため複雑です。しかし、ほとんどの開発者は、単一のキューにプッシュされるコールバック関数の数を知っていますが、これは完全に間違っています。
node.js のイベント ループ図が間違っていたため、スレッドが 2 つあると思った人もいました。 1 つは Javascript を実行し、もう 1 つはイベント ループを実行します。実際、それらはすべて 1 つのスレッドで実行されます。
もう 1 つの非常に大きな誤解は、指定された遅延が完了した後、setTimeout のコールバック関数が (おそらく OS またはカーネルによって) キューにプッシュされるということです。
一般的なイベント ループの記述にはキューが 1 つしかないため、一部の開発者は setImmediate がコールバックをワーク キューの先頭に配置すると考えています。これは完全に間違っています。JavaScript のワークキューは先入れ先出しです
イベント ループのワークフローを説明し始めるとき、そのアーキテクチャを知ることが非常に重要です。次の図は、イベント ループの実際のワークフローを示しています。
画像内の異なるボックスは異なるステージを表し、各ステージは特定の作業を実行します。各ステージにはキューがあり (ここでは主に理解を容易にするためにキューと言っていますが、実際のデータ構造はキューではない可能性があります)、Javascript はどのステージでも実行できます (アイドルと準備を除く)。図には nextTickQueue と microTaskQueue も表示されています。これらはループの一部ではなく、それらのコールバックはどの段階でも実行できます。それらは実行の優先順位が高くなります。
これで、イベント ループがさまざまなステージとさまざまなキューの組み合わせであることがわかりました。以下に各ステージについて説明します。
これは、イベント ループの開始時のフェーズです。このフェーズにバインドされているキューは、コールバックをキューにプッシュしませんが、タイマーのコールバック (setTimeout、setInterval) を保持します。ただし、タイマーを維持し、指定されたイベントに達した後にコールバックを実行するには最小のヒープを使用します。
このフェーズでは、イベント ループの pending_queue 内のコールバックが実行されます。これらのコールバックは、前の操作によってプッシュされます。たとえば、tcp に何かを書き込もうとすると、ジョブが完了し、コールバックがキューにプッシュされます。エラー処理のコールバックもここにあります。
名前はアイドルですが、ティックごとに実行されます。 Prepare は、ポーリング フェーズが開始される前にも実行されます。とにかく、これら 2 つのステージは、ノードが主にいくつかの内部操作を実行するステージです。
おそらく、イベント ループ全体の中で最も重要なフェーズは投票フェーズです。このフェーズでは、新しい受信接続 (新しいソケットの確立など) とデータ (ファイルの読み取りなど) を受け入れます。ポーリングフェーズはいくつかの異なる部分に分けることができます。
watch_queue (ポーリングフェーズにバインドされているキュー) に何かがある場合、キューが空になるかシステムが最大制限に達するまで、それらは次々に実行されます。
キューが空になると、ノードは新しい接続を待ちます。待機イベントまたはスリープ イベントはさまざまな要因によって決まります。
ポーリングの次のフェーズは、setImmediate専用のチェックフェーズです。 setImmediate コールバックを処理するために専用のキューが必要なのはなぜですか?これはポーリング フェーズの動作によるもので、これについてはプロセスのセクションで後述します。ここでは、チェックフェーズは主に setImmediate() コールバックを処理することを覚えておいてください。
コールバックのクローズ (stocket.on('close', () => {})) はすべてここで処理され、クリーンアップフェーズに似ています。
nextTickQueue のタスクは、process.nextTick() によってトリガーされるコールバックに保持されます。 microTaskQueue は、Promise によってトリガーされたコールバックを保持します。それらはいずれもイベント ループ (libUV で開発されていない) の一部ではなく、ノード内にあります。 C/C++ と JavaScript が交差する場合、それらはできるだけ早く呼び出されます。したがって、現在の操作の実行後に実行する必要があります (現在の js コールバックの実行後である必要はありません)。
コンソールでノード my-script.js を実行すると、ノードはイベント ループを設定し、イベント ループの外でメイン モジュール (my-script.js) を実行します。メインモジュールが実行されると、ノードはループがまだ生きているかどうか (イベントループで行うことが残っているかどうか) を確認します。そうでない場合は、終了コールバックの実行後に終了します。プロセス、on('exit', foo) コールバック (終了コールバック)。ただし、ループがまだ生きている場合、ノードはタイマー フェーズからループに入ります。
イベントループはタイマーフェーズに入り、タイマーキューに実行する必要があるものがあるかどうかを確認します。これは非常に簡単に聞こえますが、イベント ループは実際には、適切なコールバックを見つけるためにいくつかの手順を実行する必要があります。実際には、タイマー スクリプトは昇順にヒープ メモリに格納されます。まず実行タイマーを取得し、now-registeredTime == delta? かどうかを計算します。そうであれば、タイマーのコールバックを実行し、次のタイマーをチェックします。まだスケジュールされていないタイマーが見つかるまで、他のタイマーのチェックを停止し (タイマーは昇順に配置されているため)、次の段階に進みます。
setTimeout を 4 回呼び出して、時間 t に対して 100、200、300、400 の差がある 4 つのタイマーを作成するとします。
イベント ループが t+250 でタイマー フェーズに入ると仮定します。まず、有効期限が t+100 であるタイマー A を調べます。しかし現在は t+250 です。したがって、タイマー A にバインドされたコールバックを実行します。次にタイマー B をチェックすると、その有効期限が t+200 であることがわかり、B のコールバックも実行されます。次に、C をチェックし、その有効期限が t+300 であることが判明したので、C を終了します。タイマーが昇順に設定されているため、D のしきい値は C よりも大きいため、タイム ループは D をチェックしません。ただし、このフェーズにはシステム依存のハード制限があり、システム依存関係の最大数に達すると、未実行のタイマーがあっても次のフェーズに移行します。
タイマー フェーズの後、イベント ループは保留 I/O フェーズに入り、前の pending_queue コールバックから保留中のタスクがあるかどうかを確認します。存在する場合は、キューが空になるか、システムの上限に達するまで、順番に実行します。その後、イベント ループはアイドル ハンドラー フェーズに移行し、その後、いくつかの内部操作を実行するための準備フェーズが続きます。その後、いよいよ最も重要なフェーズである世論調査フェーズに入る可能性があります。
その名の通り、これは観察フェーズです。新しいリクエストや接続が受信されているかどうかを観察します。イベント ループがポーリング フェーズに入ると、他のフェーズと同様に、イベントが枯渇するかシステム依存関係の制限に達するまで、ファイル読み取り応答、新しいソケットまたは http 接続要求を含む watcher_queue 内のスクリプトが実行されます。実行するコールバックがない場合、ポーリングは特定の条件下でしばらく待機します。チェック キュー、保留中キュー、コールバック キューまたはアイドル ハンドラ キューを閉じているタスクが待機している場合、0 ミリ秒待機します。次に、タイマー ヒープに基づいて、最初のタイマー (利用可能な場合) を実行するまでの待機時間を決定します。最初のタイマーのしきい値を超えた場合、待つ必要はありません (最初のタイマーが実行されます)。
ポーリングフェーズが終了すると、すぐにチェックフェーズが始まります。この段階では、API setImmediate によってトリガーされたコールバックがキューにあります。キューが空になるか、依存システムの最大制限に達するまで、他のステージと同様に次々に実行されます。
チェックフェーズのタスクが完了した後、イベントループの次の目的地は、close または destroy タイプの close コールバックを処理することです。この段階でイベント ループがキュー内のコールバックの実行を終了した後、ループがまだ生きているかどうかを確認し、生きていない場合は終了します。ただし、まだ行うべき作業がある場合は、次のループに進み、タイマー フェーズに進みます。前の例のタイマー (A と B) が期限切れになったと考えると、期限切れをチェックするためにタイマー フェーズがタイマー C から開始されます。
それでは、これら 2 つのキューのコールバック関数はいつ実行されるのでしょうか?もちろん、現在のステージから次のステージに移動する前に、できるだけ速く実行します。他のステージとは異なり、システム依存のハングオーバー制限はなく、ノードは両方のキューが空になるまで実行します。ただし、nextTickQueue のタスク優先度は microTaskQueue よりも高くなります。
Javascript 開発者からよく聞く言葉は、ThreadPool です。よくある誤解は、nodejs にはすべての非同期操作を処理するプロセス プールがあるということです。しかし実際には、プロセス プールは libUV (nodejs が非同期を処理するために使用するサードパーティ ライブラリ) ライブラリ内にあります。図に示されていないのは、ループ機構の一部ではないためです。現在、すべての非同期タスクがプロセス プールによって処理されるわけではありません。 libUV は、オペレーティング システムの非同期 API を柔軟に使用して、環境をイベント駆動型に保ちます。ただし、オペレーティング システムの API は、ファイルの読み取りや DNS クエリなどを実行できません。これらは、デフォルトでは 4 つのプロセスしかないプロセス プールによって処理されます。 uv_threadpool_size の環境変数を 128 まで設定することでプロセス数を増やすことができます。
イベント ループがどのように動作するかを理解していただければ幸いです。 C 言語での同期は、JavaScript を非同期にするのに役立ちます。一度に 1 つのことしか処理しませんが、非常にブロック的です。もちろん、理論を説明する場合は例を使用するのが最もよく理解できるため、いくつかのコード スニペットを通じてこのスクリプトを理解しましょう。
setTimeout(() => {console.log('setTimeout'); }, 0); setImmediate(() => {console.log('setImmediate'); });
上記の出力を推測できますか? setTimeout が最初に出力されると思うかもしれませんが、保証はありません。なぜでしょうか?メイン モジュールを実行してタイマー フェーズに入った後、ユーザーはタイマーが期限切れになったことに気づかないか、あるいは期限切れに気づかない可能性があります。なぜ?タイマー スクリプトは、システム時間と指定したデルタ時間に基づいて登録されます。 setTimeout の呼び出しと同時に、タイマー スクリプトがメモリに書き込まれます。マシンのパフォーマンスやその上で実行されている他の操作 (ノードではない) によっては、多少の遅延が発生する可能性があります。別の時点では、ノードはタイマー フェーズ (トラバーサルの各ラウンド) に入る前に、now を現在時刻として使用して変数 now を設定するだけです。したがって、正確な時間に相当するものには何か問題があると言えるでしょう。これが不確実性の理由です。タイマー コードのコールバックで同じコードを指定すると、同じ結果が得られます。
ただし、このコードを i/o サイクルに移動すると、setTimeout の前に setImmediate コールバックが実行されることが保証されます。
fs.readFile('my-file-path.txt', () => { setTimeout(() => {console.log('setTimeout');}, 0); setImmediate(() => {console.log('setImmediate');}); });
var i = 0; var start = new Date(); function foo () { i++; if (i <p>上記の例は非常に簡単です。関数 foo を呼び出し、setImmediate を通じて 1000 まで foo を再帰的に呼び出します。私のコンピューターでは、約 6 ~ 8 ミリ秒かかりました。妖精さん、上記のコードを修正して、setImmedaite(foo) を setTimeout(foo, o) に置き換えてください。 </p><pre class="brush:php;toolbar:false">var i = 0; var start = new Date(); function foo () { i++; if (i <p> 現在、私のコンピューターでこのコードを実行するのに 1400 ミリ秒以上かかります。なぜこうなった?いずれも I/O イベントを持たず、同じである必要があります。上の 2 つの例の待機イベントは 0 です。なぜこれほど時間がかかるのでしょうか?イベントの比較を通じて逸脱が見つかり、CPU を集中的に使用するタスクにはより多くの時間がかかりました。登録されたタイマー スクリプトもイベントを消費します。タイマーの各ステージでは、タイマーを実行するかどうかを決定するためにいくつかの操作を実行する必要があります。実行時間が長くなると、ティック数も増加します。ただし、setImmediate では、キューに入ってから実行されるかのように、チェックのフェーズは 1 つだけです。 </p><h5><strong>クリップ 3 — nextTick() とタイマー実行を理解する</strong></h5><pre class="brush:php;toolbar:false">var i = 0; function foo(){ i++; if (i>20) return; console.log("foo"); setTimeout(()=>console.log("setTimeout"), 0); process.nextTick(foo); } setTimeout(foo, 2000);
上記の出力は何だと思いますか?はい、foo を出力してから setTimeout を出力します。 2 秒後、nextTickQueue によって foo() が再帰的に呼び出され、最初の foo が出力されます。すべての nextTickQueue が実行されると、他の (setTimeout コールバックなど) が実行されます。
各コールバックが実行された後、nextTickQueue のチェックが開始されるということでしょうか? コードを変更して見てみましょう。
var i = 0; function foo(){ i++; if (i>20) return; console.log("foo"); setTimeout(()=>console.log("setTimeout"), 0); process.nextTick(foo); } setTimeout(foo, 2000); setTimeout(()=>{console.log("Other setTimeout"); }, 2000);
setTimeout の後に、同じ遅延時間の Other setTimeout を出力する別の setTimeout を追加しました。保証はできませんが、最初の foo が出力された後に Other setTimeout が出力される可能性があります。同じタイマーはグループ化され、進行中のコールバック グループが実行された後に nextTickQueue が実行されます。
私たちのほとんどと同じように、イベント ループは別のスレッドにあり、コールバックをキューにプッシュし、次々に実行するものと考えられています。この記事を初めて読む読者は、JavaScript はどこで実行されるのかと疑問に思うかもしれません。前に述べたように、スレッドは 1 つだけであり、V8 または他のエンジンを使用したイベント ループ自体の Javascript コードもここで実行されます。実行は同期的であり、現在の Javascript 実行が完了していない場合、イベント ループは伝播しません。
まず、0ではなく1です。1未満または2147483647msを超える時間でタイマーを設定すると、自動的に1に設定されます。そのため、setTimeoutの遅延時間を次のように設定すると、 0 の場合は、自動的に 1 に設定されます。
また、setImmediate は余分なチェックを減らします。したがって、setImmediate の実行が速くなります。これはポーリング フェーズの後にも配置されるため、受信リクエストからの setImmediate コールバックはすぐに実行されます。
setImmediate と process.nextTick() は両方とも名前が間違っています。したがって、機能的には、setImmediate は次のティックで実行され、nextTick はすぐに実行されます。
nextTickQueue にはコールバックの実行に制限がないためです。したがって、 process.nextTick() を再帰的に実行すると、他のステージに何があるかに関係なく、プログラムはイベント ループから抜け出せない可能性があります。
タイマーを初期化することはできますが、コールバックが呼び出されることはありません。ノードが終了コールバック ステージにある場合、ノードはすでにイベント ループから飛び出しているためです。したがって、後戻りして実行することはできません。
イベント ループには作業スタックがありません
イベント ループは別のスレッドに存在せず、JavaScript の実行は実行のためにキューからコールバックをポップするほど単純ではありません。
setImmediate はコールバックをワークキューの先頭にプッシュしません。専用のステージとキューがあります。
setImmediate は次のループで実行され、nextTick は実際にはすぐに実行されます。
nextTickQueue が再帰的に呼び出された場合、ノード コードをブロックする可能性があることに注意してください。
関連する推奨事項:
JS コントロールのライフサイクルの概要_JavaScript スキル
以上がNode.jsイベントループのワークフローとライフサイクルの詳細な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。