ホームページ > 記事 > ウェブフロントエンド > Node.jsのイベントループの仕組みを解析してみる
この記事の内容は、Node.js のイベント ループの仕組みを分析するものです。必要な方は参考にしていただければ幸いです。
イベント ループのメカニズムといくつかの関連概念はブラウザの記事で詳しく紹介されていますが、主にブラウザ側の研究用です。Node 環境でも同じですか?まずデモを見てみましょう。
setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') })}, 0)setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') })}, 0)
コンパイルしてブラウザで実行した結果は次のとおりです。すでにご存知のとおりなので、ここでは詳しく説明しません。
timer1 promise1 timer2 promise2
それから、Node の下で実行します。 。 。奇妙なことに、実行結果がブラウザと異なります~
timer1 timer2 promise1 promise2
例は、ブラウザと Node.js のイベント ループのメカニズムが異なることを示しています。見てみましょう~
のイベント処理Node.js
Node.js は、js の解析エンジンとして V8 を使用し、I/O 処理に独自に設計された libuv を使用します。libuv は、いくつかの基盤となる機能をカプセル化するイベント駆動型のクロスプラットフォーム抽象化レイヤーです。さまざまなオペレーティング システムが統合された API を外部に提供します。イベント ループ メカニズムもその中に実装されています。コア ソース コード リファレンス:
int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; r = uv__loop_alive(loop); if (!r) uv__update_time(loop); while (r != 0 && loop->stop_flag == 0) { uv__update_time(loop); // timers阶段 uv__run_timers(loop); // I/O callbacks阶段 ran_pending = uv__run_pending(loop); // idle阶段 uv__run_idle(loop); // prepare阶段 uv__run_prepare(loop); timeout = 0; if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop); // poll阶段 uv__io_poll(loop, timeout); // check阶段 uv__run_check(loop); // close callbacks阶段 uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { uv__update_time(loop); uv__run_timers(loop); } r = uv__loop_alive(loop); if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break; } if (loop->stop_flag != 0) loop->stop_flag = 0; return r; }
Node.js の公式導入によると、各イベント ループには 6 つのステージが含まれています。以下の図に示すように、libuv ソース コードの実装に対応します。
timers ステージ: このステージはタイマー (setTimeout、setInterval) のコールバックを実行します
I/O コールバック ステージ: ネットワーク通信エラー コールバックなど、いくつかのシステム コール エラーを実行します。
アイドル、準備ステージ: ノードによって内部的にのみ使用されます
ポーリング ステージ: 新しい I を取得します。 /O イベント、適切 条件下では、ノードはここでブロックされます。
チェック ステージ: setImmediate() のコールバックを実行します。
クローズ コールバック ステージ: ソケットのクローズ イベント コールバックを実行します
日常の開発におけるほとんどの非同期タスクはこれら 3 つの段階で処理されるため、タイマーとポーリングに焦点を当てます。これら 3 つの段階を確認してください。
タイマー フェーズ
タイマーはイベント ループ、ノードの最初のフェーズです。 期限切れのタイマーがあるかどうかを確認し、期限切れになっている場合は、そのコールバックをタイマーのタスク キューにプッシュして実行を待ちます。 ノードによるタイマーの有効期限チェックは必ずしも信頼できるものではないため、マシン上で実行されている他のプログラムの影響を受けるか、メインスレッドがアイドル状態ではないため、事前に設定された時間に達したときにタイマーがすぐに実行されるという保証はありません。その時。たとえば、次のコードでは、setTimeout() と setImmediate() の実行順序は不定です。
setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') })
ただし、これらを I/O コールバックに置く場合は、ポーリング フェーズの後にチェック フェーズが続くため、setImmediate() を最初に実行する必要があります。
ポーリング フェーズ
ポーリング フェーズには主に 2 つの機能があります。
ポーリング キュー内のイベントの処理
タイムアウトになったタイマーがある場合、コールバック関数を実行します
偶数ループは、キューが空になるか、実行されたコールバックがシステムの上限 (上限は不明) に達するまで、ポーリング キュー内のコールバックを同期的に実行します。その後、偶数ループは次のことを確認します。プリセット setImmediate() があり、2 つの状況に分けられます。
プリセット setImmediate() がある場合、イベント ループはポーリング フェーズを終了してチェック フェーズに入り、チェックのタスク キューを実行します。フェーズ
プリセット setImmediate() がない場合、イベント ループはブロックされ、この段階で待機します。
詳細については、setImmediate() がイベントを引き起こすことはないことに注意してください。 ループはポーリングフェーズでブロックされるため、以前に設定されたタイマーは実行できなくなりませんか?したがって、投票フェーズのイベントで ループには、タイマー キューが空かどうかをチェックするチェック メカニズムがあり、タイマー キューが空でない場合はイベントが発生します。 ループはイベント ループの次のラウンドを開始します。つまり、タイマー フェーズに再び入ります。
チェック フェーズ
setImmediate() のコールバックがチェック キューに追加されます。イベント ループのフェーズ図から、チェック フェーズの実行順序がポーリングの後にあることがわかります。段階。
概要
イベント ループの各ステージにはタスク キューがあります
イベント ループが特定のステージに到達すると、そのステージのタスク キューはキューが終了するまで実行されます。または、実行されたコールバックが次のステージに進む前にシステムの上限に達します
#すべてのステージが順番に 1 回実行されると、イベント ループはティックを完了したと言われます## #それは意味がありますが、デモがないとまだ完全に理解できません。
const fs = require('fs')fs.readFile('test.txt', () => { console.log('readFile') setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) })
実行結果に疑問の余地はありません
readFile immediate timeout
Node.jsとブラウザのイベント ループの違い
前の記事、ブラウザ環境でのマイクロタスク タスクを思い出してください。キューは、各マクロタスクが実行された後に実行されます。
Node.js では、イベント ループのさまざまなステージ間でマイクロタスクが実行されます。つまり、ステージの実行後に、マイクロタスク キュー内のタスクが実行されます。 。
デモのレビュー記事の冒頭のデモを確認してください。グローバル スクリプト (main()) が実行されます。 2 つのタイマーが順番にタイマー キューに置かれ、main() が実行され、コール スタックがアイドル状態になり、タスク キューが実行を開始します。
首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;
至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、promise2。
对比浏览器端的处理过程:
process.nextTick() VS setImmediate()
In essence, the names should be swapped. process.nextTick() fires more immediately than setImmediate()
来自官方文档有意思的一句话,从语义角度看,setImmediate() 应该比 process.nextTick() 先执行才对,而事实相反,命名是历史原因也很难再变。
process.nextTick() 会在各个事件阶段之间执行,一旦执行,要直到nextTick队列被清空,才会进入到下一个事件阶段,所以如果递归调用 process.nextTick(),会导致出现I/O starving(饥饿)的问题,比如下面例子的readFile已经完成,但它的回调一直无法执行:
const fs = require('fs')const starttime = Date.now()let endtime fs.readFile('text.txt', () => { endtime = Date.now() console.log('finish reading time: ', endtime - starttime)})let index = 0function handler () { if (index++ >= 1000) return console.log(`nextTick ${index}`) process.nextTick(handler) // console.log(`setImmediate ${index}`) // setImmediate(handler)}handler()
process.nextTick()的运行结果:
nextTick 1 nextTick 2 ...... nextTick 999 nextTick 1000 finish reading time: 170
替换成setImmediate(),运行结果:
setImmediate 1 setImmediate 2 finish reading time: 80 ...... setImmediate 999 setImmediate 1000
这是因为嵌套调用的 setImmediate() 回调,被排到了下一次event loop才执行,所以不会出现阻塞。
总结
1、Node.js 的事件循环分为6个阶段
2、浏览器和Node 环境下,microtask 任务队列的执行时机不同
Node.js中,microtask 在事件循环的各个阶段之间执行
浏览器端,microtask 在事件循环的 macrotask 执行完之后执行
3、递归的调用process.nextTick()会导致I/O starving,官方推荐使用setImmediate()
以上がNode.jsのイベントループの仕組みを解析してみるの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。