画像とテキストの組み合わせは、Nodejs のイベント ループを理解するのに役立ちます。
Sep 30, 2021 am 10:22 AMこの記事では、Nodejs のイベント ループを理解するのに役立つように、画像とテキストを組み合わせて説明しています。皆様のお役に立てれば幸いです。
以下の全文は 7,000 文字です。明確な思考とエネルギーに満ちているときに読んでください。一度理解したら、長い間忘れることはないと思います。 [推奨学習: 「nodejs チュートリアル 」]
##ノード イベント ループ Nodelibuv
の下部で使用される言語は C 言語です。これは、基礎となるオペレーティング システムを操作するために使用され、オペレーティング システムのインターフェイスをカプセル化します。ノードのイベント ループもlibuv を使用して記述されるため、ノードのライフ サイクルはブラウザのライフ サイクルとは依然として異なります。 Node はオペレーティング システムを処理するため、イベント ループは比較的複雑で、独自の API がいくつかあります。
イベント ループには、オペレーティング システムが異なると微妙な違いがいくつかあります。これには、現時点ではリストされていないオペレーティング システムの知識が必要となります。
今回はJSメインスレッドにおけるNodeの動作処理のみを紹介します。ノードの他のスレッドは当面拡張されません。
イベント ループ図良い図を作ることは秘密ではありません。下の図を理解すると、イベント ループを学習できます。
イベント ループ図
イベント ループ図の構造
全体図
、最初にディレクトリ構造図を前に載せます:
ディレクトリ
#メインスレッド
メインスレッド
上の図では、いくつかの色の意味が示されています。ブロック :
main
: エントリーファイルを開始し、main 関数を実行します。-
イベントループ
: イベントループに入るかを確認します。 - 他のスレッドに保留中のアイテムがまだあるかどうかを確認します
#他のタスクがまだ進行中かどうかを確認します (タイマー、ファイル読み取り操作、その他のタスクが完了しているかどうかなど)
# #上記の状況では、他のタスクを実行しながらイベント ループに入ります。- イベント ループのプロセス: タイマーからコールバックを閉じ、円を描くまでのプロセスに従います。イベントループに移動して終了したかどうかを確認し、終了していない場合はもう一度やり直します。
- over
: すべて完了、終了
-
イベントループサークル
イベント ループ サークル
図内の灰色の円はオペレーティング システムに関連しており、この章の焦点ではありません。黄色とオレンジ色の円と、中央のオレンジ色のボックスに注目してください。サイクルは 6 つの段階を経ます:
タイマー: タイマー (setTimeout、setInterval などのコールバック関数が内部に格納されます)
- ##pending callback
idle prepare
- poll: ポーリングキュー (タイマーとチェック以外のコールバックがここに格納されます)
check: チェックフェーズ ( setImmediate を使用したコールバックはこのキューに直接入ります)
- #コールバックを閉じる
-
今回は上の赤でマークされた 3 つの重要な点にのみ焦点を当ててください。
動作原理
- 各ステージはイベント キューを維持します。各サークルはイベント キューと考えることができます。
- これはブラウザとは異なり、ブラウザには最大 2 つのキュー (マクロ キューとマイクロ キュー) があります。しかし、ノードには 6 つのキューがあります。
- キューに到着したら、キュー内に実行する必要のあるタスクがあるかどうかを確認します (つまり、コールバック関数があるかどうかを確認します)。存在する場合は、すべての実行が完了してキューがクリアされるまで、それらを順番に実行します。
- タスクがない場合は、次のキューに入って確認します。すべてのキューがチェックされるまで、ポーリングとしてカウントされます。
- これらのうち、
timers
、pending callback
、idle prepare
などは、その後poll
キューに到着します。実行の完了。
タイマー キューの動作原理
タイマーは実際のキューではなく、内部に保存されます。タイマーです。
このキューに到達するたびに、タイマー スレッド内のすべてのタイマーがチェックされます。タイマー スレッド内の複数のタイマーは時系列順に並べ替えられます。チェック処理: 各タイマーを順番に計算し、タイマーのカウント開始から現在時刻までの時間がタイマー間隔パラメーターの設定 (例: 1000ms) を満たすかどうかを計算します。タイマーのカウント開始から 1 秒までを計算します。今は1メートルのところにあります)。タイマー チェックに合格すると、そのコールバック関数が実行されます。
ポーリング キューの動作方法
- ポーリングに実行する必要があるコールバック関数がある場合、キューがクリアされるまで、コールバックは順番に実行されます。
- ポーリングに実行する必要のあるコールバック関数がない場合、キューはすでに空になっています。コールバックが他のキューに現れるまでここで待機します。
- コールバックが他のキューに現れると、ポーリングは下向きに進み、このステージを終了して次のステージに入ります。
- 他のキューにコールバックがない場合は、作業を開始する前に、いずれかのキューでコールバックが発生するまでポーリング キューで待機し続けます。 (怠け者のやり方です)
# イベントプロセスを整理する例を挙げます
setTimeout(() => { console.log('object'); }, 5000) console.log('node');
ログイン後にコピー上記のコードのイベント フローが整理されました
##メイン スレッドに入る, setTimeout()を実行すると、コールバック関数が非同期タスクとして使用されます。非同期キュータイマーのキューに入れて一時的に実行しません。- 下に進み、タイマーの背後でコンソールを実行し、「node」を出力します。
- イベント ループがあるかどうかを確認します。はい、ポーリングのサイクルを通過します。タイマー - 保留中のコールバック - アイドル準備...
- から poll
- キューまで、ループを停止して待機します。 この時点では 5 秒が経過していないため、タイマー キューにはタスクがないため、ポーリング キューに滞留し、同時に他のキューにタスクがあるかどうかを確認するためにポーリングを実行します。 。
- ポーリングと待機を続けて、イベント ループがまだ必要かどうかを確認します。必要でなければ、オーバーの終わりに達します。
この問題を理解するには、以下のコードとプロセス分析を見てください:
いつものように、最初にメイン スレッドを実行し、「ノード ライフ サイクル」を出力し、http を導入して、http サービスを作成します。コード分析は次のとおりです。setTimeout(function t1() { console.log('setTimeout'); }, 5000) console.log('node 生命周期'); const http = require('http') const server = http.createServer(function h1() { console.log('请求回调'); }); server.listen(8080)
ログイン後にコピー- 次に、イベント ループは非同期タスクがあるかどうかをチェックし、タイマー タスクとリクエスト タスクがあることを検出します。したがって、イベントループに入ります。 6 つのキューにタスクがない場合は、ポーリング キューで待機します。以下に示すように:
5 秒後、タイマーにタスクがあり、プロセスがポーリングから開始され、通過した後、下方向にリリースされます。コールバック キューをチェックして閉じ、イベント ループに到達します。- イベント ループは、非同期タスクがあるかどうかをチェックし、タイマー タスクとリクエスト タスクがあることを検出します。したがって、再度イベント ループに入ります。 タイマー キューに到着し、コールバック関数タスクがあることを確認します。次に、コールバックを順番に実行し、タイマー キューをクリアします (もちろん、5 秒後に到着するコールバックは 1 つだけなので、直接実行してください)、「setTimeout」を出力します。次の図に示すように、
#タイマー キューをクリアした後、ポーリング キューは空のキューになっているため、ポーリングはポーリング キューまで続行されます。 、ここで待っています。 後で、ユーザー要求が送信されたと仮定して、h1 コールバック関数がポーリング キューに配置されます。したがって、ポーリングには実行する必要があるコールバック関数があり、コールバックはポーリング キューがクリアされるまで順番に実行されます。 - ポーリング キューはクリアされます。この時点では、ポーリング キューは空であり、待機し続けます。
- 由于node线程一直holding在poll队列,等很长一段时间还是没有任务来临时,会自动断开等待(不自信表现),向下执行轮询流程,经过check、close callbacks后到达event loop
- 到了event loop后,检查是否有异步任务,检查发现有请求任务。(此时定时器任务已经执行完毕,所以没有了),则继续再次进入事件循环。
- 到达poll队列,再次holding……
- 再等很长时间没有任务来临,自动断开到even loop(再补充一点无任务的循环情况)
- 再次回到poll队列挂起
- 无限循环……
梳理事件循环流程图:
注意:下图中的“是否有任务”的说法表示“是否有本队列的任务”。
event loop流程梳理
再用一个典型的例子验证下流程:
const startTime = new Date(); setTimeout(function f1() { console.log('setTimeout', new Date(), new Date() - startTime); }, 200) console.log('node 生命周期', startTime); const fs = require('fs') fs.readFile('./poll.js', 'utf-8', function fsFunc(err, data) { const fsTime = new Date() console.log('fs', fsTime); while (new Date() - fsTime < 300) { } console.log('结束死循环', new Date()); });
连续运行三遍,打印结果如下:
执行流程解析:
执行全局上下文,打印「node 生命周期 + 时间」
询问是否有event loop
有,进入timers队列,检查没有计时器(cpu处理速度可以,这时还没到200ms)
轮询进入到poll,读文件还没读完(比如此时才用了20ms),因此poll队列是空的,也没有任务回调
在poll队列等待……不断轮询看有没有回调
文件读完,poll队列有了fsFunc回调函数,并且被执行,输出「fs + 时间」
在while死循环那里卡300毫秒,
死循环卡到200ms的时候,f1回调进入timers队列。但此时poll队列很忙,占用了线程,不会向下执行。
直到300ms后poll队列清空,输出「结束死循环 + 时间」
event loop赶紧向下走
再来一轮到timers,执行timers队列里的f1回调。于是看到「setTimeout + 时间」
timers队列清空,回到poll队列,没有任务,等待一会。
等待时间够长后,向下回到event loop。
event loop检查没有其他异步任务了,结束线程,整个程序over退出。
check 阶段
检查阶段(使用 setImmediate 的回调会直接进入这个队列)
check队列的实际工作原理
真正的队列,里边扔的就是待执行的回调函数的集合。类似[fn,fn]这种形式的。
每次到达check这个队列后,立即按顺序执行回调函数即可【类似于[fn1,fn2].forEach((fn)=>fn())的感觉】
所以说,setImmediate不是一个计时器的概念。
如果你去面试,涉及到Node环节,可能会遇到下边这个问题:setImmediate和setTimeout(0)谁更快。
setImmediate() 与 setTimeout(0) 的对比
- setImmediate的回调是异步的,和setTimeout回调性质一致。
- setImmediate回调在
check
队列,setTimeout回调在timers
队列(概念意义,实际在计时器线程,只是setTimeout在timers队列做检查调用而已。详细看timers的工作原理)。 - setImmediate函数调用后,回调函数会立即push到check队列,并在下次eventloop时被执行。setTimeout函数调用后,计时器线程增加一个定时器任务,下次eventloop时会在timers阶段里检查判断定时器任务是否到达时间,到了则执行回调函数。
- 综上,setImmediate的运算速度比setTimeout(0)的要快,因为setTimeout还需要开计时器线程,并增加计算的开销。
二者的效果差不多。但是执行顺序不定
观察以下代码:
setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); });
多次反复运行,执行效果如下:
顺序不定
可以看到多次运行,两句console.log打印的顺序不定。
这是因为setTimeout的间隔数最小填1,虽然下边代码填了0。但实际计算机执行当1ms算。(这里注意和浏览器的计时器区分。在浏览器中,setInterval的最小间隔数为10ms,小于10ms则会被设置为10;设备供电状态下,间隔最小为16.6ms。)
以上代码,主线程运行的时候,setTimeout函数调用,计时器线程增加一个定时器任务。setImmediate函数调用后,其回调函数立即push到check队列。主线程执行完毕。
eventloop判断时,发现timers和check队列有内容,进入异步轮询:
第一种情况:等到了timers里这段时间,可能还没有1ms的时间,定时器任务间隔时间的条件不成立所以timers里还没有回调函数。继续向下到了check队列里,这时候setImmediate的回调函数早已等候多时,直接执行。而再下次eventloop到达timers队列,定时器也早已成熟,才会执行setTimeout的回调任务。于是顺序就是「setImmediate -> setTimeout」。
第二种情况:但也有可能到了timers阶段时,超过了1ms。于是计算定时器条件成立,setTimeout的回调函数被直接执行。eventloop再向下到达check队列执行setImmediate的回调。最终顺序就是「setTimeout -> setImmediate」了。
所以,只比较这两个函数的情况下,二者的执行顺序最终结果取决于当下计算机的运行环境以及运行速度。
二者时间差距的对比代码
------------------setTimeout测试:------------------- let i = 0; console.time('setTimeout'); function test() { if (i < 1000) { setTimeout(test, 0) i++ } else { console.timeEnd('setTimeout'); } } test(); ------------------setImmediate测试:------------------- let i = 0; console.time('setImmediate'); function test() { if (i < 1000) { setImmediate(test) i++ } else { console.timeEnd('setImmediate'); } } test();
运行观察时间差距:
setTimeout与setImmediate时间差距
可见setTimeout远比setImmediate耗时多得多
这是因为setTimeout不仅有主代码执行的时间消耗。还有在timers队列里,对于计时器线程中各个定时任务的计算时间。
结合poll队列的面试题(考察timers、poll和check的执行顺序)
如果你看懂了上边的事件循环图,下边这道题难不倒你!
// 说说下边代码的执行顺序,先打印哪个? const fs = require('fs') fs.readFile('./poll.js', () => { setTimeout(() => console.log('setTimeout'), 0) setImmediate(() => console.log('setImmediate')) })
上边这种代码逻辑,不管执行多少次,肯定都是先执行setImmediate。
先执行setImmediate
因为fs各个函数的回调是放在poll队列的。当程序holding在poll队列后,出现回调立即执行。
回调内执行setTimeout和setImmediate的函数后,check队列立即增加了回调。
回调执行完毕,轮询检查其他队列有内容,程序结束poll队列的holding向下执行。
check是poll阶段的紧接着的下一个。所以在向下的过程中,先执行check阶段内的回调,也就是先打印setImmediate。
到下一轮循环,到达timers队列,检查setTimeout计时器符合条件,则定时器回调被执行。
nextTick 与 Promise
说完宏任务,接下来说下微任务
- 二者都是「微队列」,执行异步微任务。
- 二者不是事件循环的一部分,程序也不会开启额外的线程去处理相关任务。(理解:promise里发网络请求,那是网络请求开的网络线程,跟Promise这个微任务没关系)
- 微队列设立的目的就是让一些任务「马上」、「立即」优先执行。
- nextTick与Promise比较,nextTick的级别更高。
nextTick表现形式
process.nextTick(() => {})
Promise表现形式
Promise.resolve().then(() => {})
如何参与事件循环?
事件循环中,每执行一个回调前,先按序清空一次nextTick和promise。
// 先思考下列代码的执行顺序 setImmediate(() => { console.log('setImmediate'); }); process.nextTick(() => { console.log('nextTick 1'); process.nextTick(() => { console.log('nextTick 2'); }) }) console.log('global'); Promise.resolve().then(() => { console.log('promise 1'); process.nextTick(() => { console.log('nextTick in promise'); }) })
最终顺序:
global
nextTick 1
nextTick 2
promise 1
nextTick in promise
setImmediate
两个问题:
基于上边的说法,有两个问题待思考和解决:
每走一个异步宏任务队列就查一遍nextTick和promise?还是每执行完 宏任务队列里的一个回调函数就查一遍呢?
如果在poll的holding阶段,插入一个nextTick或者Promise的回调,会立即停止poll队列的holding去执行回调吗?
上边两个问题,看下边代码的说法
setTimeout(() => { console.log('setTimeout 100'); setTimeout(() => { console.log('setTimeout 100 - 0'); process.nextTick(() => { console.log('nextTick in setTimeout 100 - 0'); }) }, 0) setImmediate(() => { console.log('setImmediate in setTimeout 100'); process.nextTick(() => { console.log('nextTick in setImmediate in setTimeout 100'); }) }); process.nextTick(() => { console.log('nextTick in setTimeout100'); }) Promise.resolve().then(() => { console.log('promise in setTimeout100'); }) }, 100) const fs = require('fs') fs.readFile('./1.poll.js', () => { console.log('poll 1'); process.nextTick(() => { console.log('nextTick in poll ======'); }) }) setTimeout(() => { console.log('setTimeout 0'); process.nextTick(() => { console.log('nextTick in setTimeout'); }) }, 0) setTimeout(() => { console.log('setTimeout 1'); Promise.resolve().then(() => { console.log('promise in setTimeout1'); }) process.nextTick(() => { console.log('nextTick in setTimeout1'); }) }, 1) setImmediate(() => { console.log('setImmediate'); process.nextTick(() => { console.log('nextTick in setImmediate'); }) }); process.nextTick(() => { console.log('nextTick 1'); process.nextTick(() => { console.log('nextTick 2'); }) }) console.log('global ------'); Promise.resolve().then(() => { console.log('promise 1'); process.nextTick(() => { console.log('nextTick in promise'); }) }) /** 执行顺序如下 global ------ nextTick 1 nextTick 2 promise 1 nextTick in promise setTimeout 0 // 解释问题1. 没有上边的nextTick和promise,setTimeout和setImmediate的顺序不一定,有了以后肯定是0先开始。 // 可见,执行一个队列之前,就先检查并执行了nextTick和promise微队列 nextTick in setTimeout setTimeout 1 nextTick in setTimeout1 promise in setTimeout1 setImmediate nextTick in setImmediate poll 1 nextTick in poll ====== setTimeout 100 nextTick in setTimeout100 promise in setTimeout100 setImmediate in setTimeout 100 nextTick in setImmediate in setTimeout 100 setTimeout 100 - 0 nextTick in setTimeout 100 - 0 */
以上代码执行多次,顺序不变,setTimeout和setImmediate的顺序都没变。
执行顺序及具体原因说明如下:
global
:主线程同步任务,率先执行没毛病nextTick 1
:执行异步宏任务之前,清空异步微任务,nextTick优先级高,先行一步nextTick 2
:执行完上边这句代码,又一个nextTick微任务,立即率先执行promise 1
:执行异步宏任务之前,清空异步微任务,Promise的优先级低,所以在nextTick完了以后立即执行nextTick in promise
:清空Promise队列的过程中,遇到nextTick微任务,立即执行、清空setTimeout 0
: 解释第一个问题. 没有上边的nextTick和promise,只有setTimeout和setImmediate时他俩的执行顺序不一定。有了以后肯定是0先开始。可见,执行一个宏队列之前,就先按顺序检查并执行了nextTick和promise微队列。等微队列全部执行完毕,setTimeout(0)的时机也成熟了,就被执行。nextTick in setTimeout
:执行完上边这句代码,又一个nextTick微任务,立即率先执行 【这种回调函数里的微任务,我不能确定是紧随同步任务执行的;还是放到微任务队列,等下一个宏任务执行前再清空的他们。但是顺序看上去和立即执行他们一样。不过我比较倾向于是后者:先放到微任务队列等待,下一个宏任务执行前清空他们。】setTimeout 1
:因为执行微任务耗费时间,导致此时timers里判断两个0和1的setTimeout计时器已经结束,所以两个setTimeout回调都已加入队列并被执行nextTick in setTimeout1
:执行完上边这句代码,又一个nextTick微任务,立即率先执行 【可能是下一个宏任务前清空微任务】promise in setTimeout1
:执行完上边这句代码,又一个Promise微任务,立即紧随执行 【可能是下一个宏任务前清空微任务】setImmediate
:poll队列回调时机未到,先行向下到check队列,清空队列,立即执行setImmediate回调nextTick in setImmediate
:执行完上边这句代码,又一个nextTick微任务,立即率先执行 【可能是下一个宏任务前清空微任务】poll 1
:poll队列实际成熟,回调触发,同步任务执行。nextTick in poll
:执行完上边这句代码,又一个nextTick微任务,立即率先执行 【可能是下一个宏任务前清空微任务】setTimeout 100
:定时器任务到达时间,执行回调。并在回调里往微任务推入了nextTick、Promise,往宏任务的check里推入了setImmediate的回调。并且也开启了计时器线程,往timers里增加了下一轮回调的可能。nextTick in setTimeout100
:宏任务向下前,率先执行定时器回调内新增的微任务-nextTick 【这里就能确定了,是下一个宏任务前清空微任务的流程】promise in setTimeout100
:紧接着执行定时器回调内新增的微任务-Promise 【清空完nextTick清空Promise的顺序】setImmediate in setTimeout 100
:这次setImmediate比setTimeout(0)先执行的原因是:流程从timers向后走到check队列,已经有了setImmediate的回调,立即执行。nextTick in setImmediate in setTimeout 100
:执行完上边这句代码,又一个nextTick微任务,下一个宏任务前率先清空微任务setTimeout 100 - 0
:轮询又一次回到timers,执行100-0的回调。nextTick in setTimeout 100 - 0
:执行完上边这句代码,又一个nextTick微任务,下一个宏任务前率先清空微任务。
扩展:为什么有了setImmediate还要有nextTick和Promise?
一开始设计的时候,setImmediate充当了微队列的作用(虽然他不是)。设计者希望执行完poll后立即执行setImmediate(当然现在也确实是这么表现的)。所以起的名字叫Immediate
,表示立即
的意思。
但是后来问题是,poll里可能有N个任务连续执行,在执行期间想要执行setImmediate是不可能的。因为poll队列不停,流程不向下执行。
于是出现nextTick,真正的微队列概念。但此时,immediate的名字被占用了,所以名字叫nextTick(下一瞬间)。事件循环期间,执行任何一个队列之前,都要检查他是否被清空。其次是Promise。
面试题
最后,检验学习成果的面试题来了
async function async1() { console.log('async start'); await async2(); console.log('async end'); } async function async2(){ console.log('async2'); } console.log('script start'); setTimeout(() => { console.log('setTimeout 0'); }, 0) setTimeout(() => { console.log('setTimeout 3'); }, 3) setImmediate(() => { console.log('setImmediate'); }) process.nextTick(() => { console.log('nextTick'); }) async1(); new Promise((res) => { console.log('promise1'); res(); console.log('promise2'); }).then(() => { console.log('promise 3'); }); console.log('script end'); // 答案如下 // - // - // - // - // - // - // - // - // - // - // - // - /** script start async start async2 promise1 promise2 script end nextTick async end promise 3 // 后边这仨的运行顺序就是验证你电脑运算速度的时候了。 速度最好(执行上边的同步代码 + 微任务 + 计时器运算用了不到0ms): setImmediate setTimeout 0 setTimeout 3 速度中等(执行上边的同步代码 + 微任务 + 计时器运算用了0~3ms以上): setTimeout 0 setImmediate setTimeout 3 速度较差(执行上边的同步代码 + 微任务 + 计时器运算用了3ms以上): setTimeout 0 setTimeout 3 setImmediate */
思维脑图 - Node生命周期核心阶段
更多编程相关知识,请访问:编程视频!!
以上が画像とテキストの組み合わせは、Nodejs のイベント ループを理解するのに役立ちます。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

人気の記事

人気の記事

ホットな記事タグ

メモ帳++7.3.1
使いやすく無料のコードエディター

SublimeText3 中国語版
中国語版、とても使いやすい

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

SublimeText3 Mac版
神レベルのコード編集ソフト(SublimeText3)

ホットトピック











Nodejs インストール ディレクトリ内の npm ファイルと npm.cmd ファイルの違いは何ですか?
