JavaScriptの動作メカニズムの詳細な説明とイベントループについての簡単な説明

JavaScriptの動作メカニズムの詳細な説明とイベントループについての簡単な説明

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よりよく理解するには、次の図を参照してください。

上の図では、メインスレッドが実行されると、 heapstackが生成されます。スタック内のコードはさまざまな外部 API を呼び出し、さまざまなイベント ( clickloaddone ) を「タスク キュー」に追加します。スタック内のコードが実行されている限り、メインスレッドは「タスク キュー」を読み取り、それらのイベントに対応するコールバック関数を順番に実行します。

実行スタック (同期タスク) 内のコードは、常に「タスク キュー」(非同期タスク) を読み取る前に実行されます。以下の例をご覧ください。

    var req = 新しい XMLHttpRequest();
    リクエストをオープンします('GET', url);    
    req.onload = 関数 (){};    
    req.onerror = 関数 (){};    
    要求を送信します。


上記のコードのreq.sendメソッドは、サーバーにデータを送信するAjax操作です。これは非同期タスクであり、現在のスクリプトのすべてのコードが実行された後にのみ、システムが「タスク キュー」を読み取ります。したがって、以下と同等です。

    var req = 新しい XMLHttpRequest();
    リクエストをオープンします('GET', url);
    要求を送信します。
    req.onload = 関数 (){};    
    req.onerror = 関数 (){};   


つまり、コールバック関数 ( onloadonerror ) を指定する部分が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.nextTicketImmediate 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);

上記のコードでは、 setImmediatesetTimeout(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

上記のコードでは、 setImmediatesetTimeoutが 1 つのsetImmediateにカプセル化されており、その実行結果は常に 1--TIMEOUT FIRED--2 です。このとき、関数 A はタイムアウト前にトリガーされる必要があります。関数 2 がTIMEOUT FIRED後に配置されている(つまり、関数 B がtimeoutの後にトリガーされている)のは、 setImmediate常にイベントを次のEvent Loopに登録するため、関数 A とtimeout同じループ ラウンドで実行され、関数 B は次のループ ラウンドで実行されるためです。

このことから、 process.nextTicksetImmediateの重要な違いがわかります。複数の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 操作メカニズム イベントループ

<<:  Linux xargsコマンドの使用

>>:  MySQL の一般的なログの概要

推薦する

ab ツールを使用してサーバー上で API ストレス テストを実行します。

目次1 システムスループットの簡単な紹介2 試験方法2.1 クライアントテストツール2.1.1 GE...

JavaScript es6 における var、let、const の違いの詳細な説明

まず、よくある質問は、ECMAScript と JavaScript の関係は何ですか? ECMAS...

iptables および firewalld ツールを使用して Linux ファイアウォール接続ルールを管理する

ファイアウォールファイアウォールは一連のルールです。パケットが保護されたネットワーク空間に出入りする...

Nginx URL 書き換えメカニズムの原理と使用例

URL 書き換えは、Web サイトの優先ドメインを決定するのに役立ちます。同じリソース ページの複数...

HTML チュートリアル: 順序付きリスト

<br />原文: http://andymao.com/andy/post/103.h...

JavaScript のシングルトン デザイン パターン

目次1. デザインパターンとは何ですか? 2. デザインパターンの5つの設計原則(SOLID) 3....

Vue3ナビゲーションバーコンポーネントのカプセル化実装方法

参考までに、Vue3でナビゲーションバーコンポーネントをカプセル化し、スクロールバーのスクロールに合...

VMware Workstation 14 Pro は CentOS 7.0 をインストールします

VMware Workstation 14 ProにCentOS 7.0をインストールする具体的な方...

Linux での Python のアップグレードと pip のインストールの詳細な説明

Linuxバージョンのアップグレード: 1. まず、Linuxオペレーティングシステムに付属するPy...

MySQL マスタースレーブスイッチチャネルの問題の解決策

VIP を設定した後、アクティブ/スタンバイの切り替え中に表示されるエラー メッセージは次のとおりで...

熟練デザイナーの7つの原則(1):フォントデザイン

まあ、あなたはデザインの達人かもしれませんし、あるいはそれは大げさすぎるかもしれませんが、少なくとも...

HTML フォーム タグの使用方法を学ぶチュートリアル

HTML のフォームを使用して、ユーザーからさまざまな種類の入力情報を収集できます。フォームは、実際...

MySQLでSELECT文が実行される仕組み

目次1. マクロの観点からMySQLを分析する2. SQL ステートメントを実行するには、どの程度の...

Nginxを使用してストリーミングメディアサーバーを構築し、ライブブロードキャスト機能を実現する

前面に書かれた近年、ライブストリーミング業界は非常に人気が高まっています。伝統的な業界でのライブスト...

CentOS7 で MySQL のスケジュールされた自動バックアップを実装する方法

実稼働環境で起こる最も嬉しいことは、シナリオによっては、更新または削除時にパラメータを無視せざるを得...