ホームページ  >  記事  >  ウェブフロントエンド  >  JSとNode.jsのイベントループを詳しく解説

JSとNode.jsのイベントループを詳しく解説

小云云
小云云オリジナル
2017-12-13 09:30:471483ブラウズ

js の event Loop は、Chrome と Node で setTimeoutPromise を使用してプログラムを実行するときに実行結果が異なるという問題を引き起こします。そのため、この記事この記事では、Nodejs の event Loop メカニズムを紹介し、例を通して JS と Node.js のイベントの原理と使用法を詳細に分析します。 event loop,引出了chrome与node中运行具有setTimeoutPromise的程序时候执行结果不一样的问题,从而引出了Nodejs的event loop机制,本篇文章通过实例给大家详细分析了JS与Node.js中的事件的原理以及用法,希望能帮助到大家。

console.log(1)
setTimeout(function() {
 new Promise(function(resolve, reject) {
 console.log(2)
 resolve()
 })
 .then(() => {
 console.log(3)
 })
}, 0)
setTimeout(function() {
 console.log(4)
}, 0)
// chrome中运行:1 2 3 4
// Node中运行: 1 2 4 3

chrome和Node执行的结果不一样,这就很有意思了。

1. JS 中的任务队列

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

2. 任务队列 event loop

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。主线程不断重复上面的第三步。

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

3. 定时器 setTimeoutsetInterval

定时器功能主要由setTimeout()setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。

setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()

console.log('1')
setTimeout(function() {
 console.log('2')
 new Promise(function(resolve) {
 console.log('4')
 resolve()
 }).then(function() {
 console.log('5')
 })
 setTimeout(() => {
 console.log('haha')
 })
 new Promise(function(resolve) {
 console.log('6')
 resolve()
 }).then(function() {
 console.log('66')
 })
})
setTimeout(function() {
 console.log('hehe')
}, 0)
new Promise(function(resolve) {
 console.log('7')
 resolve()
}).then(function() {
 console.log('8')
})
setTimeout(function() {
 console.log('9')
 new Promise(function(resolve) {
 console.log('11')
 resolve()
 }).then(function() {
 console.log('12')
 })
})
new Promise(function(resolve) {
 console.log('13')
 resolve()
}).then(function() {
 console.log('14')
})
// node1 : 1,7,13,8,14,2,4,6,hehe,9,11,5,66,12,haha // 结果不稳定
// node2 : 1,7,13,8,14,2,4,6,hehe,5,66,9,11,12,haha // 结果不稳定
// node3 : 1,7,13,8,14,2,4,6,5,66,hehe,9,11,12,haha // 结果不稳定
// chrome : 1,7,13,8,14,2,4,6,5,66,hehe,9,11,12,haha

ChromeとNodeの実行結果が違っていてとても興味深いです。 1. JS のタスクキュー

🎜🎜🎜 JavaScript 言語の大きな特徴は、シングルスレッドであること、つまり同時に 1 つのことしか実行できないことです。では、なぜ JavaScript は複数のスレッドを持てないのでしょうか?これにより効率が向上します。
🎜🎜JavaScript の単一スレッドはその目的に関連しています。ブラウザーのスクリプト言語としての JavaScript の主な目的は、ユーザーと対話して DOM を操作することです。これにより、シングルスレッドのみが可能であることが決まります。そうでない場合は、非常に複雑な同期の問題が発生します。たとえば、JavaScript に同時に 2 つのスレッドがあるとします。1 つのスレッドが特定の DOM ノードにコンテンツを追加し、もう 1 つのスレッドがそのノードを削除するとします。この場合、ブラウザーはどちらのスレッドを使用すればよいでしょうか。
🎜🎜そのため、複雑さを避けるために、JavaScript は誕生以来シングルスレッドであり、これがこの言語の中心的な機能となっており、今後も変更されることはありません。
🎜🎜マルチコア CPU の計算能力を活用するために、HTML5 は Web Worker 標準を提案しています。これにより、JavaScript スクリプトは複数のスレッドを作成できますが、子スレッドはメインスレッドによって完全に制御され、 DOM の操作は許可されていません。したがって、この新しい標準は JavaScript のシングルスレッドの性質を変更しません。 🎜🎜🎜🎜2. タスクキューイベントループ🎜🎜🎜🎜 シングルスレッドとは、すべてのタスクをキューに入れる必要があり、次のタスクが実行される前に前のタスクが完了することを意味します。前のタスクに時間がかかると、次のタスクも待たされることになります。
🎜🎜つまり、すべてのタスクは、同期タスク (synchronous) と非同期タスク (asynchronous) の 2 種類に分けることができます。同期タスクは、メイン スレッドで実行するためにキューに入れられたタスクを指します。次のタスクは、前のタスクが実行された後にのみ実行できます。非同期タスクは、メイン スレッドには入らないが「タスク キュー タスク」に入るタスクを指します。 「タスクキュー」が非同期タスクを実行できることをメインスレッドに通知した場合にのみ、タスクは実行のためにメインスレッドに入ります。
🎜🎜具体的には、非同期実行の動作仕組みは以下の通りです。 (同期実行についても同様です。非同期タスクがなければ非同期実行とみなされるためです。) 🎜🎜すべての同期タスクはメインスレッドで実行され、実行コンテキスト スタックを形成します。メインスレッドの他に「タスクキュー」もあります。非同期タスクに実行結果がある限り、イベントは「タスク キュー」に配置されます。 「実行スタック」内のすべての同期タスクが実行されると、システムは「タスク キュー」を読み取り、その中にどのようなイベントがあるかを確認します。これらの対応する非同期タスクは待機状態を終了し、実行スタックに入り、実行を開始します。メインスレッドは上記の 3 番目のステップを繰り返し続けます。 🎜

🎜🎜メインスレッドが空である限り、「タスクキュー」を読み取ります。これがJavaScriptの実行メカニズムです。このプロセスは繰り返され続けます。 🎜🎜🎜🎜3. タイマー🎜🎜 setTimeoutsetInterval🎜🎜 タイマー関数は主に setTimeout()setInterval で構成されます。 ( )これら 2 つの関数を完了するための内部動作メカニズムはまったく同じです。違いは、前者で指定されたコードが 1 回実行されるのに対し、後者は繰り返し実行されることです。 🎜🎜setTimeout(fn,0) は、メインスレッドの利用可能な最も早いアイドル時間に実行されるタスク、つまり、できるだけ早く実行されるタスクを指定することを意味します。 「タスクキュー」の最後にイベントを追加するため、同期タスクと「タスクキュー」内の既存のイベントが処理されるまで実行されません。 🎜🎜 HTML5 標準では、setTimeout() の 2 番目のパラメータの最小値 (最短間隔) が指定されています。これは 4 未満であってはなりませんミリ秒。この値を下回ると、自動的に増加します。これより前の古いブラウザでは、最小間隔が 10 ミリ秒に設定されていました。さらに、これらの DOM 変更 (特にページの再レンダリングを伴う変更) は通常、すぐには実行されず、16 ミリ秒ごとに実行されます。現時点では、requestAnimationFrame() を使用した方が、setTimeout() よりも効果が高くなります。 🎜🎜 setTimeout() はイベントを「タスクキュー」に挿入するだけであることに注意してください。メインスレッドは、指定されたコールバック関数を実行する前に、現在のコード (実行スタック) が完了するまで待機する必要があります。現在のコードに時間がかかる場合は、長時間かかる可能性があるため、setTimeout() で指定された時間にコールバック関数が実行されることを保証する方法はありません。 🎜🎜🎜🎜4. Node.jsのイベントループ🎜🎜🎜

イベント ポーリングは主にイベント キューをポーリングします。イベント プロデューサーはイベントをキューに入れ、キューの反対側にイベント コンシューマーと呼ばれるスレッドがあり、キューにイベントがあるかどうかを継続的にクエリします。イベントがある場合は、ブロック操作が実行中に現在のスレッドの読み取りキューに影響を与えるのを防ぐために、イベント コンシューマ スレッドはスレッド プールにこれらのブロック操作を特別に実行させます。

JavaScript フロントエンドと Node.js のメカニズムは、このイベント ポーリング モデルに似ています。Node.js はシングルスレッド、つまりイベント コンシューマーはシングル スレッドで継続的にポーリングされると考える人もいます。ブロック操作がある場合はどうすればよいですか? それは現在のシングルスレッドの実行をブロックしませんか?

実際、Node.js の下部にはスレッド プールがあり、このスレッド プールはさまざまなブロック操作を実行するために特別に使用されます。これは、イベント ポーリングや一部のタスクの実行を行うシングルスレッドのメイン スレッドには影響しません。スレッド プールの操作が完了すると、イベント プロデューサーとしても機能し、操作の結果を同じキューに入れます。

つまり、イベント ポーリング イベント ループには 3 つのコンポーネントが必要です:

イベント キュー。これは FIFO モデルに属し、一方の端はイベント データをプッシュし、もう一方の端はこのキューを通じてのみ通信します。 、非同期疎結合に属します。キューの読み取りポーリング スレッド、イベント コンシューマー、およびイベント ループの主役。別のスレッド プールであるスレッド プールは、長時間のタスク、重いタスク、および重い物理的作業を実行するために特別に使用されます。

Node.jsもシングルスレッドのイベントループですが、その動作メカニズムはブラウザ環境とは異なります。

上の図によると、Node.jsの動作仕組みは以下の通りです。

V8 エンジンは JavaScript スクリプトを解析します。解析されたコードは Node API を呼び出します。 libuv ライブラリは、ノード API の実行を担当します。異なるタスクを異なるスレッドに割り当ててイベントループ(イベントループ)を形成し、タスクの実行結果を非同期でV8エンジンに返します。その後、V8 エンジンは結果をユーザーに返します。

node.js のコアは実際には libuvこのライブラリであることがわかります。このライブラリは C で書かれており、マルチスレッド テクノロジを使用できますが、JavaScript アプリケーションはシングルスレッドです。

Nodejs の非同期タスク実行プロセス:

ユーザーが作成したコードはシングルスレッドですが、nodejs は内部的にシングルスレッドではありません。

イベント メカニズム:

Node.js は、各リクエストの作業を実行するために複数のスレッドを使用しません。代わりに、すべての作業をイベント キューに追加し、キュー イベントをループする別のスレッドを持ちます。イベント ループ スレッドは、イベント キューの先頭のエントリを取得して実行し、次のエントリを取得します。実行時間が長いコードや I/O がブロックされているコードを実行するとき

Node.js では、HTTP リクエストなどを含むデータベース ファイル システムなどの I/O 操作のためにイベントのキューを常にポーリングしているスレッドが 1 つだけであるため、ブロックや待機が発生しやすいこれらの操作がこの単一スレッドに実装されている場合、それらは確実にブロックされ、他のタスクの実行に影響を与えます。JavaScript/Node.js は実行を基礎となるスレッド プールに委任し、スレッド プールにコールバックを指示します。このようにして、単一スレッドは他の処理を実行し続け、これらのブロック操作が完了すると、単一スレッドがキューからイベントを読み取り続け、結果がキューに入れられます。これらのブロック操作の結果。これらの操作結果はコールバック関数の入力パラメーターとして使用され、コールバック関数がアクティブ化されて実行されます。

Node.js のシングル スレッドは、キュー イベントの読み取りだけでなく、コールバック関数の実行も行うことに注意してください。これは、マルチスレッド モードのシングル スレッドとは異なる大きな機能です。はキュー イベントの取得のみを担当し、他のことは実行しません。特にマルチコアの場合、1 つの CPU コアがキュー イベントの読み取りを担当します。 core は、アクティブ化されたタスクの実行を担当します。この方法は、非常に高価な CPU コンピューティング タスクに最適です。次に、Node..js の実行アクティブ化タスク、つまりコールバック関数内のタスクは、ポーリングを担当する単一スレッドで引き続き実行されます。これにより、変換などの CPU 負荷の高いタスクの実行が防止されます。 JSON から他のデータ形式への変換など、これらのタスクはイベント ポーリングの効率に影響します。

5. Nodejs の特徴

NodeJS の顕著な特徴: 非同期メカニズム、イベント駆動。

イベントポーリングのプロセス全体は新しいユーザーの接続をブロックせず、接続を維持する必要もありません。この仕組みに基づいて、理論上、NodeJS は接続を要求するユーザーに次々に応答することができるため、Java や PHP プログラムよりも高い同時実行性をサポートできます。

イベントキューの維持にもコストがかかりますが、NodeJSはシングルスレッドなのでイベントキューが長くなると応答を得るまでに時間がかかり、やはり同時実行量が不足します。

RESTful API是NodeJS最理想的应用场景,可以处理数万条连接,本身没有太多的逻辑,只需要请求API,组织数据进行返回即可。

6. 实例

看一个具体实例:

console.log('1')
setTimeout(function() {
 console.log('2')
 new Promise(function(resolve) {
 console.log('4')
 resolve()
 }).then(function() {
 console.log('5')
 })
 setTimeout(() => {
 console.log('haha')
 })
 new Promise(function(resolve) {
 console.log('6')
 resolve()
 }).then(function() {
 console.log('66')
 })
})
setTimeout(function() {
 console.log('hehe')
}, 0)
new Promise(function(resolve) {
 console.log('7')
 resolve()
}).then(function() {
 console.log('8')
})
setTimeout(function() {
 console.log('9')
 new Promise(function(resolve) {
 console.log('11')
 resolve()
 }).then(function() {
 console.log('12')
 })
})
new Promise(function(resolve) {
 console.log('13')
 resolve()
}).then(function() {
 console.log('14')
})
// node1 : 1,7,13,8,14,2,4,6,hehe,9,11,5,66,12,haha // 结果不稳定
// node2 : 1,7,13,8,14,2,4,6,hehe,5,66,9,11,12,haha // 结果不稳定
// node3 : 1,7,13,8,14,2,4,6,5,66,hehe,9,11,12,haha // 结果不稳定
// chrome : 1,7,13,8,14,2,4,6,5,66,hehe,9,11,12,haha


chrome的运行比较稳定,而node环境下运行不稳定,可能会出现两种情况。

chrome运行的结果的原因是Promiseprocess.nextTick()的微任务Event Queue运行的权限比普通宏任务Event Queue权限高,如果取事件队列中的事件的时候有微任务,就先执行微任务队列里的任务,除非该任务在下一轮的Event Loop中,微任务队列清空了之后再执行宏任务队列里的任务。

相关推荐:

Node.js事件循环教程

javascript事件循环之强制梳理

深入理解Node.js 事件循环和回调函数

以上がJSとNode.jsのイベントループを詳しく解説の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。