目次- 1. JavaScript がシングルスレッドなのはなぜですか?
- 2. タスクキュー
- 3. イベントとコールバック関数
- 4. イベントループ
- 5. タイマー
- 6. Node.js イベントループ
1. JavaScript がシングルスレッドなのはなぜですか? JavaScript 言語の主な特徴はシングルスレッド、つまり同時に実行できるのは 1 つのことだけであることです。では、なぜ JavaScript は複数のスレッドを持つことができないのでしょうか?これにより効率が向上します。 JavaScript的單線程 、それがその目的に関連しています。ブラウザ スクリプト言語としてのJavaScript の主な目的は、ユーザーと対話し、 DOM を操作することです。これにより、シングルスレッドのみが可能になり、そうでない場合は非常に複雑な同期の問題が発生します。たとえば、 JavaScript 同時に 2 つのスレッドがあり、1 つのスレッドがDOM ノードにコンテンツを追加し、もう 1 つのスレッドがノードを削除するとします。ブラウザーはどちらのスレッドに従うべきでしょうか? そのため、複雑さを避けるために、 JavaScript 誕生以来シングルスレッド化されています。これは言語のコア機能となり、今後も変更されることはありません。 HTML5 、マルチコアCPU の計算能力を活用するために、 JavaScript スクリプトが複数のスレッドを作成できるようにするWeb Worker 標準を提案しています。ただし、子スレッドはメインスレッドによって完全に制御され、 DOM を操作することはできません。したがって、この新しい標準では、JavaScript のシングルスレッドの性質は変わりません。 2. タスクキューシングルスレッドとは、すべてのタスクをキューに入れる必要があり、前のタスクが完了した後にのみ次のタスクが実行されることを意味します。前のタスクに長い時間がかかる場合、次のタスクは待機する必要があります。 キューが計算量が多いためにCPU がビジー状態になっている場合は問題ありませんが、IO デバイス (入力デバイスと出力デバイス) が非常に遅い (ネットワークからデータを読み取るAjax 操作など) ため、ほとんどの場合CPU アイドル状態になり、結果が出るまで待ってからさらに実行する必要があります。 JavaScript 言語の設計者は、この時点でメイン スレッドが IO デバイスを完全に無視し、待機中のタスクを一時停止し、後でキューに入れられたタスクを最初に実行できることに気づきました。 IO デバイスが結果を返すまで待機し、戻って中断されたタスクの実行を続行します。 したがって、すべてのタスクは、同期タスク ( synchronous ) とasynchronous タスク (asynchronous) の 2 つのタイプに分けられます。同期タスクとは、メインスレッドで実行するためにキューに入れられたタスクを指します。次のタスクは、前のタスクが完了した後にのみ実行できます。非同期タスクとは、メインスレッドに入らずに「 task queue 」に入るタスクを指します。「タスクキュー」がメインスレッドに非同期タスクが実行可能であることを通知した場合にのみ、タスクはメインスレッドに入って実行されます。 具体的には、非同期実行の動作の仕組みは以下のようになります。 (同期実行の場合も同様です。非同期タスクのない非同期実行と見なすことができます。) (1)すべての同期タスクはメインスレッド上で実行され、 execution context stack スタックを形成します。 (2)メインスレッドの他に「 task queue 」も存在します。非同期タスクに実行結果がある限り、イベントは「タスク キュー」に配置されます。 (3)「実行スタック」内のすべての同期タスクが実行されると、システムは「タスクキュー」を読み取り、そこにどのようなイベントが含まれているかを確認します。対応する非同期タスクは待機状態を終了し、実行スタックに入り、実行を開始します。 (4)メインスレッドは上記のステップ3を継続的に繰り返します。
下の図はメインスレッドとタスクキューの概略図です。 
メインスレッドが空である限り、 JavaScript の動作メカニズムである「タスクキュー」を読み取ります。このプロセスは繰り返されます。 3. イベントとコールバック関数「タスク キュー」はイベント キューです (メッセージ キューとも呼ばれます)。IO デバイスがタスクを完了すると、「タスク キュー」にイベントが追加され、関連する非同期タスクが「実行スタック」に入ることができることを示します。メイン スレッドは「タスク キュー」を読み取ります。つまり、そこにどのようなイベントが含まれているかを読み取ります。 「タスク キュー」内のイベントには、IO デバイスからのイベントだけでなく、ユーザーによって生成されたイベント (マウスのクリック、ページのスクロールなど) も含まれます。コールバック関数が指定されている限り、これらのイベントは発生すると「タスク キュー」に入り、メイン スレッドによる読み取りを待機します。 いわゆる「 callback 関数」は、メインスレッドによって中断されるコードです。非同期タスクではコールバック関数を指定する必要があります。メイン スレッドが非同期タスクの実行を開始すると、対応するコールバック関数が実行されます。 「タスク キュー」は先入れ先出しのデータ構造です。先頭のイベントは、最初にメイン スレッドによって読み取られます。メインスレッドの読み取り処理は基本的に自動です。実行スタックがクリアされていれば、「タスクキュー」の最初のイベントは自動的にメインスレッドに入ります。ただし、後述する「タイマー」機能により、メインスレッドはまず実行時間をチェックする必要があります。特定のイベントは、指定された時間になったときにのみメインスレッドに戻ることができます。 4. イベントループメインスレッドは「タスクキュー」からイベントを読み取り、このプロセスは継続的であるため、この動作メカニズム全体はイベントループとも呼ばれます。 Event Loop よりよく理解するには、次の図を参照してください。 
上の図では、メインスレッドが実行されると、 heap とstack が生成されます。スタック内のコードはさまざまな外部 API を呼び出し、さまざまなイベント ( click 、 load 、 done ) を「タスク キュー」に追加します。スタック内のコードが実行されている限り、メインスレッドは「タスク キュー」を読み取り、それらのイベントに対応するコールバック関数を順番に実行します。 実行スタック (同期タスク) 内のコードは、常に「タスク キュー」(非同期タスク) を読み取る前に実行されます。以下の例をご覧ください。
var req = 新しい XMLHttpRequest();
リクエストをオープンします('GET', url);
req.onload = 関数 (){};
req.onerror = 関数 (){};
要求を送信します。
上記のコードのreq.send メソッドは、サーバーにデータを送信するAjax 操作です。これは非同期タスクであり、現在のスクリプトのすべてのコードが実行された後にのみ、システムが「タスク キュー」を読み取ります。したがって、以下と同等です。
var req = 新しい XMLHttpRequest();
リクエストをオープンします('GET', url);
要求を送信します。
req.onload = 関数 (){};
req.onerror = 関数 (){};
つまり、コールバック関数 ( onload とonerror ) を指定する部分がsend() メソッドの前か後かは関係ありません。これは、それらが実行スタックの一部であり、システムは常に「タスク キュー」を読み取る前にそれらを実行するためです。 5. タイマー「タスク キュー」では、非同期タスクのイベントを配置するだけでなく、特定のコードが実行されるのにかかる時間を指定する時間指定イベントも配置できます。これは「タイマー」関数と呼ばれ、決まった時間に実行されるコードです。 タイマー機能は主に、 setTimeout() とsetInterval() 2 つの関数によって実行されます。これらの関数の内部動作メカニズムはまったく同じです。違いは、前者で指定されたコードは 1 回実行されるのに対し、後者で指定されたコードは繰り返し実行されることです。以下では主にsetTimeout()。 setTimeout() は 2 つのパラメータを受け入れます。1 つ目はコールバック関数、2 つ目は実行を延期するミリ秒数です。
コンソールログ(1);
タイムアウトを設定します(function(){console.log(2);},1000);
コンソールログ(3);
上記コードの実行結果は 1、3、2 となります。これはsetTimeout() 2 行目の実行を 1000 ミリ秒後に延期するためです。 setTimeout() の 2 番目のパラメータが 0 に設定されている場合、現在のコードが実行された後 (実行スタックがクリアされた後)、指定されたコールバック関数が直ちに実行されます (間隔は 0 ミリ秒)。
タイムアウトを設定します(function(){console.log(1);}, 0);
コンソールログ(2);
上記のコードの実行結果は常に 2, 1 になります。これは、システムが 2 行目が実行された後にのみ、「タスク キュー」内のコールバック関数を実行するためです。 つまり、 setTimeout(fn,0) の意味は、タスクをメイン スレッドの最も早い利用可能なアイドル時間、つまりできるだけ早く実行することを指定することです。 「タスク キュー」の最後にイベントを追加するため、同期タスクと「タスク キュー」内の既存のイベントが処理されるまで実行されません。 HTML5 標準では、 setTimeout() の 2 番目のパラメータの最小値 (最短間隔) は 4 ミリ秒未満であってはならないと規定されています。この値より小さい場合は、自動的に増加します。以前のバージョンのブラウザでは、最小間隔は 10 ミリ秒に設定されていました。さらに、DOM の変更 (特にページの再レンダリングを伴うもの) は通常、すぐに実行されるのではなく、16 ミリ秒ごとに実行されます。現時点では、 requestAnimationFrame() 方がsetTimeout()。 setTimeout() イベントを「タスク キュー」に挿入するだけであることに注意してください。メイン スレッドは、現在のコード (実行スタック) が実行されるまで、指定されたコールバック関数を実行しません。現在のコードに時間がかかる場合は、長時間待機する必要がある場合があり、setTimeout() で指定された時間にコールバック関数が実行されることを保証する方法はありません。 6. Node.js イベントループNode.js にもシングルスレッドのEvent Loop がありますが、その動作の仕組みはブラウザ環境とは異なります。
下の図をご覧ください 
上図によると、Node.jsの動作の仕組みは以下のようになります。 (1)V8エンジンはJavaScript スクリプトを解析します。 (2)解析後、コードはNode APIを呼び出します。 (3) libuv ライブラリはNode API の実行を担当します。異なるタスクを異なるスレッドに割り当ててEvent Loop を形成し、タスクの実行結果を非同期で V8 エンジンに返します。 (4)V8エンジンは結果をユーザーに返します。
setTimeout メソッドとsetInterval メソッドに加えて、 Node.js 「タスク キュー」に関連するprocess.nextTick とetImmediate 2 つのメソッドも提供します。これらは、「タスク キュー」についての理解を深めるのに役立ちます。 process.nextTick メソッドは、現在の「実行スタック」の最後、つまり次のEvent Loop の前 (メイン スレッドが「タスク キュー」を読み取ります) にコールバック関数をトリガーできます。つまり、指定されたタスクは常にすべての非同期タスクの前に実行されます。 setImmediate メソッドは、現在の「タスク キュー」の末尾にイベントを追加します。つまり、指定されたタスクは常に次のEvent Loop で実行されます。これは、 setTimeout(fn, 0) と非常によく似ています。以下の例を参照してください ( via StackOverflow )。
process.nextTick(関数A() {
コンソールログ(1);
process.nextTick(関数 B(){console.log(2);});
});
setTimeout(関数タイムアウト() {
console.log('タイムアウトが発生しました');
}, 0)
// 1
// 2
//タイムアウトが発生しました
上記のコードでは、 process.nextTick メソッドで指定されたコールバック関数は常に現在の「実行スタック」の終了時にトリガーされるため、 setTimeout で指定されたコールバック関数のタイムアウト前に関数 A が実行されるだけでなく、関数 B もtimeout 前に実行されます。つまり、複数のprocess.nextTick ステートメントがある場合 (ネストされているかどうかに関係なく)、すべてが現在の「実行スタック」で実行されます。 さて、setImmediateを見てみましょう
setImmediate(関数A() {
コンソールログ(1);
setImmediate(関数B(){console.log(2);});
});
setTimeout(関数タイムアウト() {
console.log('タイムアウトが発生しました');
}, 0);
上記のコードでは、 setImmediate とsetTimeout(fn,0) はそれぞれコールバック関数 A とtimeout を追加し、どちらも次のEvent Loop でトリガーされます。では、どのコールバック関数が最初に実行されるのでしょうか?答えは不明です。実行結果は1--TIMEOUT FIRED--2 またはTIMEOUT FIRED--1--2。 混乱を招くのは、 Node.js ドキュメントでは、 setImmediate で指定されたコールバック関数は常にsetTimeout の前に配置されると記載されていることです。実際には、これは再帰呼び出しでのみ発生します。
setImmediate(関数(){
setImmediate(関数A() {
コンソールログ(1);
setImmediate(関数B(){console.log(2);});
});
setTimeout(関数タイムアウト() {
console.log('タイムアウトが発生しました');
}, 0);
});
// 1
//タイムアウトが発生しました
// 2
上記のコードでは、 setImmediate とsetTimeout が 1 つのsetImmediate にカプセル化されており、その実行結果は常に 1--TIMEOUT FIRED--2 です。このとき、関数 A はタイムアウト前にトリガーされる必要があります。関数 2 がTIMEOUT FIRED 後に配置されている(つまり、関数 B がtimeout の後にトリガーされている)のは、 setImmediate 常にイベントを次のEvent Loop に登録するため、関数 A とtimeout 同じループ ラウンドで実行され、関数 B は次のループ ラウンドで実行されるためです。 このことから、 process.nextTick とsetImmediate の重要な違いがわかります。複数のprocess.nextTick ステートメントは常に現在の「実行スタック」で 1 回実行されますが、複数のsetImmediate を実行するには複数のループが必要になる場合があります。実際、これがまさに Node.js バージョン 10.0 でsetImmediate メソッドが追加された理由です。そうしないと、次のような再帰呼び出しprocess.nextTick が終了せず、メイン スレッドは「イベント キュー」をまったく読み取らなくなります。
process.nextTick(関数foo() {
プロセスは次のティックを実行します(foo);
});
実際、 process.nextTick,Node.js 警告をスローし、 setImmediate 変更するように求めます。 また、 process.nextTick で指定したコールバック関数は現在の「イベントループ」でトリガーされ、 setImmediate は次の「イベントループ」でトリガーされるため、前者の方が後者よりも常に早く発生し、実行効率が高いことは明らかです(「タスクキュー」を確認する必要がないため)。 以上でJavaScript 動作の仕組みの詳しい説明と、イベントループについての簡単な説明は終了です。JavaScript JavaScript 動作の仕組みやEvent Loop についてさらに詳しく知りたい方は、123WORDPRESS.COM の過去の記事を検索するか、以下の関連記事を引き続きご覧ください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。 以下もご興味があるかもしれません:- JavaScriptの動作の仕組みのイベントループ(Event Loop)の詳しい解説
- Javascript 操作メカニズム イベントループ
|