JavaScript はマルチスレッド言語ではなく、シングルスレッド言語です。 JavaScript はブラウザのスクリプト言語であり、そのインタプリタはシングルスレッドです。JavaScript の主な目的はユーザーと対話して DOM を操作することですが、JavaScript はシングルスレッドのみであることが決定され、そうでない場合は非常に複雑な同期の問題が発生します。 。
このチュートリアルの動作環境: Windows7 システム、JavaScript バージョン 1.8.5、Dell G3 コンピューター。
JavaScript はマルチスレッド言語ではなく、シングルスレッド言語です。 ブラウザの JavaScript インタープリタはシングルスレッドであるため、JavaScript 言語はマルチスレッドもサポートしていません。
JavaScript 言語の主な機能はシングルスレッドです。これは、同時に 1 つのことしか実行できないことを意味します。
では、なぜ JavaScript は複数のスレッドを持てないのでしょうか?これにより効率が向上します。
JavaScript の単一スレッドは、その目的に関連しています。ブラウザーのスクリプト言語としての JavaScript の主な目的は、ユーザーと対話して DOM を操作することです。これにより、シングルスレッドのみが可能であることが決まります。そうでない場合は、非常に複雑な同期の問題が発生します。
マルチコア CPU のコンピューティング能力を活用するために、HTML5 は Web Worker 標準を提案しています。これにより、JavaScript スクリプトは複数のスレッドを作成できますが、子スレッドはメインスレッドによって完全に制御されるため、 DOM を操作しないでください。したがって、この新しい標準は JavaScript のシングルスレッドの性質を変更しません。
タスク キュー
単一スレッドとは、すべてのタスクをキューに入れる必要があり、前のタスクが完了するまで次のタスクは実行されないことを意味します。前のタスクに時間がかかると、次のタスクも待たされることになります。
キューが大量の計算によるものであり、CPU がビジー状態である場合は、そのことを忘れてください。ただし、多くの場合、IO デバイス (入出力デバイス) が非常に遅いため、CPU はアイドル状態になります (たとえば、 , Ajax 操作はネットワークから読み取る (データを取得する) ため、続行する前に結果が出るまで待つ必要があります。
JavaScript 言語の設計者は、現時点では、メインスレッドが IO デバイスを完全に無視し、待機中のタスクを一時停止し、後のタスクを最初に実行できることに気づきました。 IO デバイスが結果を返すまで待ってから、戻って中断されたタスクの実行を続行します。
つまり、すべてのタスクは 2 つのタイプに分類できます。1 つは同期タスク (synchronous)、もう 1 つは非同期タスク (asynchronous) です。
同期タスクは、メイン スレッドで実行するためにキューに入れられたタスクを指します。次のタスクは、前のタスクが実行された後にのみ実行できます。
非同期タスクは、メイン スレッドに入ることができないタスクを指します。 「タスクキュー」に入ったタスクは、「タスクキュー」がメインスレッドに非同期タスクの実行が可能であることを通知した場合にのみ、タスクはメインスレッドに入って実行されます。
具体的には、非同期実行の動作仕組みは以下のとおりです。 (同期実行についても同様です。非同期タスクがなければ非同期実行とみなすことができるためです。)
(1) すべての同期タスクはメインスレッドで実行され、実行コンテキスト スタックを形成します。
(2) メインスレッドの他に「タスクキュー」もあります。非同期タスクに実行結果がある限り、イベントは「タスク キュー」に配置されます。
(3) 「実行スタック」内のすべての同期タスクが完了すると、システムは「タスク キュー」を読み取り、その中にどのようなイベントがあるかを確認します。これらの対応する非同期タスクは待機状態を終了し、実行スタックに入り、実行を開始します。
(4) メインスレッドは引き続き上記の 3 番目のステップを繰り返します。
次の図は、メインスレッドとタスクキューの概略図です。
#メインスレッドが空である限り、JavaScript の実行メカニズムである「タスクキュー」を読み取ります。このプロセスは繰り返され続けます。
イベントとコールバック関数
「タスク キュー」はイベントのキューです (メッセージのキューとしても理解できます)。IO デバイスがタスクを完了すると、「タスク キュー」にイベントを追加すると、関連する非同期タスクが「実行スタック」に入ることができることを示します。メインスレッドは「タスクキュー」を読み取ります。これは、その中のイベントを読み取ることを意味します。
「タスク キュー」内のイベントには、IO デバイス イベントに加えて、ユーザーが生成したイベント (マウス クリック、ページ スクロールなど) も含まれます。コールバック関数が指定されている限り、これらのイベントは発生時に「タスクキュー」に入り、メインスレッドによる読み取りを待ちます。
いわゆる「コールバック関数」(コールバック) は、メインスレッドによってハングアップされるコードです。非同期タスクではコールバック関数を指定する必要があり、メインスレッドが非同期タスクの実行を開始すると、対応するコールバック関数が実行されます。
「タスクキュー」は先入れ先出しのデータ構造であり、最初にランク付けされたイベントがメインスレッドによって最初に読み取られます。メインスレッドの読み取り処理は基本的に自動で行われ、実行スタックがクリアされるとすぐに、「タスクキュー」の最初のイベントが自動的にメインスレッドに入ります。ただし、後述する「タイマー」機能により、メインスレッドは実行時間を確認する必要があり、イベントによっては指定された時間が経過しないとメインスレッドに戻ることができません。
イベントループ
メイン スレッドは「タスク キュー」からイベントを読み取ります。このプロセスは周期的であるため、実行メカニズム全体はイベント ループとも呼ばれます。
イベント ループをより深く理解するには、下の図をご覧ください。
上の図では、メインスレッドの実行中にヒープとスタックが生成されます。スタック内のコードはさまざまな外部 API を呼び出します。それらは「」にあります。タスク「さまざまなイベント (クリック、ロード、完了) をキューに追加する」。スタック内のコードが実行されている限り、メインスレッドは「タスクキュー」を読み取り、それらのイベントに対応するコールバック関数を順番に実行します。
実行スタック (同期タスク) 内のコードは、常に「タスク キュー」 (非同期タスク) を読み取る前に実行されます。以下の例を見てください。
var req = new XMLHttpRequest(); req.open('GET', url); req.onload = function (){}; req.onerror = function (){}; req.send();
上記のコードの req.send メソッドは、サーバーにデータを送信する Ajax 操作です。これは非同期タスクです。つまり、システムはすべてのコードが完了した後でのみ「タスク キュー」を読み取ります。現在のスクリプトが実行されます。したがって、以下の記述に相当します。
var req = new XMLHttpRequest(); req.open('GET', url); req.send(); req.onload = function (){}; req.onerror = function (){};
言い換えると、コールバック関数 (onload と onerror) を指定する部分が send() メソッドの前であっても後であっても関係ありません。これは、コールバック関数は実行スタックの一部であり、システムの一部であるためです。常に「タスクキュー」を読み取る前にそれらを実行します。
タイマー
「タスク キュー」は、非同期タスクのイベントを配置するだけでなく、時間指定のイベント、つまり特定のコードの実行時間を指定することもできます。後。これは「タイマー」関数と呼ばれ、定期的に実行されるコードです。
タイマー関数は、主に setTimeout() と setInterval() の 2 つの関数によって完成されます。内部の動作メカニズムはまったく同じです。違いは、前者の
で指定されるコードが次のとおりであることです。一度実行されると、後者は繰り返し実行されます。以下では主に setTimeout() について説明します。
setTimeout() は 2 つのパラメータを受け入れます。1 つ目はコールバック関数で、2 つ目は実行を遅らせるミリ秒数です。
console.log(1); setTimeout(function(){console.log(2);},1000); console.log(3);
setTimeout() が 2 行目の実行を 1000 ミリ秒後まで遅らせるため、上記のコードの実行結果は 1、3、2 になります。
setTimeout() の 2 番目のパラメータが 0 に設定されている場合、現在のコードが実行された (実行スタックがクリアされた) 後、指定されたコールバック関数がすぐに実行されます (0 ミリ秒間隔)
setTimeout(function(){console.log(1);}, 0); console.log(2);
2 行目の実行後にのみシステムが「タスク キュー」内のコールバック関数を実行するため、上記のコードの実行結果は常に 2、1 になります。
つまり、setTimeout(fn,0) の意味は、メインスレッドの利用可能な最も早いアイドル時間に実行されるタスク、つまりできるだけ早く実行されるタスクを指定することです。 「タスクキュー」の最後にイベントを追加するため、同期タスクと「タスクキュー」内の既存のイベントが処理されるまで実行されません。
HTML5 標準では、setTimeout() の第 2 パラメータの最小値(最短間隔)は 4 ミリ秒以上と規定されており、この値を下回る場合は自動的に増加します。これより前の古いブラウザでは、最小間隔が 10 ミリ秒に設定されていました。さらに、これらの DOM 変更 (特にページの再レンダリングを伴う変更) は通常、すぐには実行されず、16 ミリ秒ごとに実行されます。このとき、setTimeout()よりもrequestAnimationFrame()を使用した方が効果が高くなります。
setTimeout() はイベントを「タスク キュー」に挿入するだけであることに注意してください。メイン スレッドは、現在のコード (実行スタック) の実行が終了するまで待機してから、コールバック関数を実行する必要があります。と指定します。現在のコードに時間がかかる場合は、長時間かかる可能性があるため、setTimeout() で指定された時間にコールバック関数が実行されることを保証する方法はありません。
Node.jsのイベントループ
Node.jsもシングルスレッドのイベントループですが、動作の仕組みがブラウザ環境とは異なります。
下の図をご覧ください。
上記の図によると、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 経由)。
process.nextTick(function A() { console.log(1); process.nextTick(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0) // 1 // 2 // TIMEOUT FIRED
上面代码中,由于process.nextTick方法指定的回调函数,总是在当前”执行栈”的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前”执行栈”执行。
现在,再看setImmediate。
setImmediate(function A() { console.log(1); setImmediate(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0);
上面代码中,setImmediate与setTimeout(fn,0)各自添加了一个回调函数A和timeout,都是在下一次Event Loop触发。那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是1–TIMEOUT FIRED–2,也可能是TIMEOUT FIRED–1–2。
令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。
setImmediate(function (){ setImmediate(function A() { console.log(1); setImmediate(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0); }); // 1 // TIMEOUT FIRED // 2
上面代码中,setImmediate和setTimeout被封装在一个setImmediate里面,它的运行结果总是1–TIMEOUT FIRED–2,这时函数A一定在timeout前面触发。至于2排在TIMEOUT FIRED的后面(即函数B在timeout后面触发),是因为setImmediate总是将事件注册到下一轮Event Loop,所以函数A和timeout是在同一轮Loop执行,而函数B在下一轮Loop执行。
我们由此得到了process.nextTick和setImmediate的一个重要区别:多个process.nextTick语句总是在当前”执行栈”一次执行完,多个setImmediate可能则需要多次loop才能执行完。事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取”事件队列”!
process.nextTick(function foo() { process.nextTick(foo); });
事实上,现在要是你写出递归的process.nextTick,Node.js会抛出一个警告,要求你改成setImmediate。
另外,由于process.nextTick指定的回调函数是在本次”事件循环”触发,而setImmediate指定的是在下次”事件循环”触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查”任务队列”)。
【相关推荐:javascript学习教程】
以上がJavaScript はマルチスレッド言語ですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。