1. JavaScript はなぜシングルスレッドなのでしょうか?
JavaScript 言語の主な機能はシングルスレッドです。これは、一度に 1 つのことしか実行できないことを意味します。では、なぜ JavaScript は複数のスレッドを持てないのでしょうか?これにより効率が向上します。
JavaScript の単一スレッドはその目的に関連しています。ブラウザーのスクリプト言語としての JavaScript の主な目的は、ユーザーと対話し、DOM を操作することです。これにより、シングルスレッドのみが可能であることが決まります。そうでない場合は、非常に複雑な同期の問題が発生します。たとえば、JavaScript に同時に 2 つのスレッドがあるとします。1 つのスレッドが特定の DOM ノードにコンテンツを追加し、もう 1 つのスレッドがそのノードを削除するとします。この場合、ブラウザーはどちらのスレッドを使用すればよいでしょうか。
したがって、複雑さを避けるために、JavaScript は誕生以来シングルスレッドであり、これは言語の中核的な機能となっており、今後も変更されることはありません。
マルチコア CPU の計算能力を活用するために、HTML5 は Web Worker 標準を提案しています。これにより、JavaScript スクリプトは複数のスレッドを作成できますが、子スレッドはメインスレッドによって完全に制御され、子スレッドを操作してはなりません。ドム。したがって、この新しい標準は JavaScript のシングルスレッドの性質を変更しません。
2. タスクキュー
シングルスレッドとは、すべてのタスクをキューに入れる必要があり、前のタスクが完了するまで次のタスクは実行されないことを意味します。前のタスクに時間がかかると、次のタスクも待たされることになります。
キューが大量の計算によるもので、CPU がビジー状態である場合は、それを忘れてください。しかし、多くの場合、IO デバイス (入出力デバイス) が非常に遅いため、CPU はアイドル状態になります (Ajax 操作など)。ネットワークからデータを読み取る)、続行する前に結果が出るまで待つ必要があります。
JavaScript 言語の設計者は、現時点では、CPU が IO デバイスを完全に無視し、待機中のタスクを一時停止し、後のタスクを最初に実行できることに気づきました。 IO デバイスが結果を返すまで待ってから、戻って中断されたタスクの実行を続行します。
その結果、JavaScript には 2 つの実行方法があります。1 つは CPU が順番に実行し、前のタスクが終了してから次のタスクが実行される方法 (同期実行と呼ばれます)、もう 1 つは CPU がタスクをスキップする方法です。待ち時間が長い場合は、後続のタスクを最初に処理します。これを非同期実行と呼びます。どの実行方法を使用するかを選択するのはプログラマの責任です。
非同期実行の具体的な動作仕組みは以下の通りです。 (同期実行についても、非同期タスクなしの非同期実行と見なすことができるため、同じことが当てはまります。)
(1) すべてのタスクはメインスレッドで実行され、実行コンテキストスタックを形成します。
(2) メインスレッドの他に「タスクキュー」があります。システムは非同期タスクを「タスクキュー」に入れ、後続のタスクを実行し続けます。
(3) 「実行スタック」内のすべてのタスクが実行されると、システムは「タスクキュー」を読み取ります。このとき、非同期タスクが待ち状態を終了していれば、「タスクキュー」から実行スタックに入り、実行を再開します。
(4) メインスレッドは上記の 3 番目のステップを繰り返し続けます。
下の図はメインスレッドとタスクキューの模式図です。
メインスレッドが空である限り、「タスクキュー」を読み取ります。これが JavaScript の実行メカニズムです。このプロセスは繰り返され続けます。
3. イベントとコールバック関数
「タスク キュー」は本質的にイベント キュー (メッセージ キューとしても理解できます) であり、IO デバイスがタスクを完了すると、関連する非同期タスクが入ることができることを示すイベントが「タスク キュー」に追加されます。実行スタック」。メインスレッドは「タスクキュー」を読み取ります。これは、その中のイベントを読み取ることを意味します。
「タスク キュー」内のイベントには、IO デバイス イベントに加えて、ユーザーが生成したイベント (マウス クリック、ページ スクロールなど) も含まれます。コールバック関数が指定されている限り、これらのイベントは発生時に「タスクキュー」に入り、メインスレッドによる読み取りを待ちます。
いわゆる「コールバック関数」は、メインスレッドによってハングアップされるコードです。非同期タスクはコールバック関数を指定する必要があります。非同期タスクが「タスク キュー」から実行スタックに戻ると、コールバック関数が実行されます。
「タスクキュー」は先入れ先出しのデータ構造で、最初にランク付けされたイベントが最初にメインスレッドに返されます。メインスレッドの読み取りプロセスは基本的に自動であり、実行スタックがクリアされるとすぐに、「タスクキュー」の最初のイベントが自動的にメインスレッドに戻ります。ただし、後述する「タイマー」機能により、メインスレッドは実行時間をチェックする必要があり、特定のイベントは指定された時間にメインスレッドに返さなければなりません。
4. イベントループ
メインスレッドは「タスクキュー」からイベントを読み取ります。このプロセスは周期的であるため、動作メカニズム全体はイベントループとも呼ばれます。
イベント ループをよりよく理解するために、下の図をご覧ください (フィリップ ロバーツのスピーチ「助けて、イベント ループにはまってしまいました」から引用)。
上の図では、メインスレッドの実行中にヒープとスタックが生成され、スタック内のコードはさまざまな外部 API を呼び出して「タスク キュー」にさまざまなイベント (クリック、ロードなど) を追加します。 "。 終わり)。スタック内のコードが実行されている限り、メインスレッドは「タスクキュー」を読み取り、それらのイベントに対応するコールバック関数を順番に実行します。
実行スタック内のコードは、常に「タスクキュー」を読み取る前に実行されます。以下の例を見てください。
タイマー関数は、主に setTimeout() と setInterval() の 2 つの関数によって実行されます。それらの内部動作メカニズムはまったく同じです。違いは、前者で指定されたコードが 1 回実行されるのに対し、後者は繰り返し実行されることです。 。以下では主に setTimeout() について説明します。
setTimeout() は 2 つのパラメータを受け入れます。1 つ目はコールバック関数で、2 つ目は実行を遅らせるミリ秒数です。コードをコピーします
上記のコードの実行結果は常に 2, 1 になります。これは、2 行目の実行後にのみシステムが「タスク キュー」内のコールバック関数を実行するためです。
HTML5 標準では、setTimeout() の第 2 パラメータの最小値 (最短間隔) を 4 ミリ秒以上にすることが規定されており、この値より小さい場合は自動的に増加します。これより前の古いブラウザでは、最小間隔が 10 ミリ秒に設定されていました。
さらに、これらの DOM 変更 (特にページの再レンダリングを伴う変更) は通常、すぐには実行されず、16 ミリ秒ごとに実行されます。この時点では、setTimeout() よりも requestAnimationFrame() を使用した方が効果が高くなります。
setTimeout() はイベントを「タスク キュー」に挿入するだけであることに注意してください。メイン スレッドは、指定されたコールバック関数を実行する前に、現在のコード (実行スタック) の実行が終了するまで待機する必要があります。現在のコードに時間がかかる場合は、長時間かかる可能性があるため、setTimeout() で指定された時間にコールバック関数が実行されることを保証する方法はありません。
6. Node.jsのイベントループ
Node.js もシングルスレッドのイベント ループですが、その動作メカニズムはブラウザ環境とは異なります。
下の図をご覧ください (@BusyRich による)。
上の図によると、Node.js の動作メカニズムは次のとおりです。
(1) V8 エンジンは JavaScript スクリプトを解析します。
(2) 解析されたコードはノード API を呼び出します。
(3) libuv ライブラリは、ノード API の実行を担当します。異なるタスクを異なるスレッドに割り当ててイベントループ(イベントループ)を形成し、タスクの実行結果を非同期でV8エンジンに返します。
(4) V8 エンジンは結果をユーザーに返します。
2 つのメソッド setTimeout と setInterval に加えて、Node.js は「タスク キュー」に関連する他の 2 つのメソッド、process.nextTick と setImmediate も提供します。これらは、「タスクキュー」についての理解を深めるのに役立ちます。
process.nextTick メソッドは、メインスレッドが次回「タスクキュー」を読み取る前に、現在の「実行スタック」の最後でコールバック関数をトリガーできます。つまり、指定したタスクは常にすべての非同期タスクの前に発生します。 setImmediate メソッドは、現在の「タスク キュー」の最後でコールバック関数をトリガーします。つまり、指定されたタスクは、次回メイン スレッドが「タスク キュー」を読み取るときに常に実行されます。これは setTimeout( とよく似ています) fn、0) 。以下の例を参照してください (StackOverflow 経由)。
setTimeout(関数タイムアウト() {
console.log('タイムアウトが発生しました');
}, 0)
// 1
// 2
// タイムアウトが発生しました
次に、setImmediate を見てください。
setTimeout(関数タイムアウト() {
console.log('タイムアウトが発生しました');
}, 0)
// 1
// タイムアウトが発生しました
// 2
上記のコードには、2 つの setImmediate があります。最初の setImmediate は、コールバック関数 A が現在の「タスク キュー」(次の「イベント ループ」) の終了時にトリガーされることを指定します。次に、setTimeout は、コールバック関数のタイムアウトが現在の「タスク」の終了時にトリガーされることも指定します。 queue" であるため、出力結果では TIMEOUT FIRED が 1 よりも下にランクされます。 TIMEOUT FIRED に次ぐ 2 位については、setImmediate のもう 1 つの重要な機能によるものです。「イベント ループ」は、setImmediate で指定された 1 つのコールバック関数のみをトリガーできます。
ここから重要な違いがわかります。複数の process.nextTick ステートメントは常に一度に実行されますが、複数の setImmediate ステートメントは複数回実行する必要があります。実際、これが Node.js バージョン 10.0 で setImmediate メソッドを追加した理由です。そうしないと、次のような process.nextTick への再帰呼び出しが無限に行われ、メイン スレッドが「イベント キュー」をまったく読み取れなくなります。