溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何理解Nodejs中的事件循環(huán)

發(fā)布時(shí)間:2021-09-30 10:34:27 來源:億速云 閱讀:152 作者:柒染 欄目:web開發(fā)

這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)碛嘘P(guān)如何理解Nodejs中的事件循環(huán),文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

Node事件循環(huán)

Node底層使用的語(yǔ)言libuv,是一個(gè)c++語(yǔ)言。他用來操作底層的操作系統(tǒng),封裝了操作系統(tǒng)的接口。Node的事件循環(huán)也是用libuv來寫的,所以Node生命周期和瀏覽器的還是有區(qū)別的。

因?yàn)镹ode和操作系統(tǒng)打交道,所以事件循環(huán)比較復(fù)雜,也有一些自己特有的API。
事件循環(huán)在不同的操作系統(tǒng)里有一些細(xì)微的差異。這將涉及到操作系統(tǒng)的知識(shí),暫時(shí)不表。 本次只介紹JS主線程中,Node的運(yùn)作流程。Node的其他線程暫時(shí)也不擴(kuò)展。

事件循環(huán)圖

說好的一張圖,也不賣關(guān)子。下邊這張圖搞清楚了,事件循環(huán)就學(xué)會(huì)了。

如何理解Nodejs中的事件循環(huán)

事件循環(huán)圖

如何理解Nodejs中的事件循環(huán)

事件循環(huán)圖-結(jié)構(gòu)

為了讓大家先有個(gè)大局觀,先貼一張目錄結(jié)構(gòu)圖在前邊:

如何理解Nodejs中的事件循環(huán)

目錄

接下來詳細(xì)展開說說

主線程

如何理解Nodejs中的事件循環(huán)

主線程

上圖中,幾個(gè)色塊的含義:

  • main:?jiǎn)?dòng)入口文件,運(yùn)行主函數(shù)

  • event loop:檢查是否要進(jìn)入事件循環(huán)

    • 檢查其他線程里是否還有待處理事項(xiàng)

    • 檢查其他任務(wù)是否還在進(jìn)行中(比如計(jì)時(shí)器、文件讀取操作等任務(wù)是否完成)

    • 有以上情況,進(jìn)入事件循環(huán),運(yùn)行其他任務(wù)
      事件循環(huán)的過程:沿著從timers到close callbacks這個(gè)流程,走一圈。到event loop看是否結(jié)束,沒結(jié)束再走一圈。

  • over:所有的事情都完畢,結(jié)束

事件循環(huán) 圈

如何理解Nodejs中的事件循環(huán)

事件循環(huán) 圈

圖中灰色的圈跟操作系統(tǒng)有關(guān)系,不是本章解析重點(diǎn)。重點(diǎn)關(guān)注黃色、橙色的圈還有中間橘黃的方框。

我們把每一圈的事件循環(huán)叫做「一次循環(huán)」、又叫「一次輪詢」、又叫「一次Tick」。

一次循環(huán)要經(jīng)過六個(gè)階段:
  • timers:計(jì)時(shí)器(setTimeout、setInterval等的回調(diào)函數(shù)存放在里邊)

  • pending callback

  • idle prepare

  • poll:輪詢隊(duì)列(除timers、check之外的回調(diào)存放在這里)

  • check:檢查階段(使用 setImmediate 的回調(diào)會(huì)直接進(jìn)入這個(gè)隊(duì)列)

  • close callbacks

如何理解Nodejs中的事件循環(huán)

本次我們只關(guān)注上邊標(biāo)紅的三個(gè)重點(diǎn)。

工作原理
  • 每一個(gè)階段都會(huì)維護(hù)一個(gè)事件隊(duì)列。可以把每一個(gè)圈想象成一個(gè)事件隊(duì)列。

  • 這就和瀏覽器不一樣了,瀏覽器最多兩個(gè)隊(duì)列(宏隊(duì)列、微隊(duì)列)。但是在node里邊有六個(gè)隊(duì)列

  • 到達(dá)一個(gè)隊(duì)列后,檢查隊(duì)列內(nèi)是否有任務(wù)(也就是看下是否有回調(diào)函數(shù))需要執(zhí)行。如果有,就依次執(zhí)行,直到全部執(zhí)行完畢、清空隊(duì)列。

  • 如果沒有任務(wù),進(jìn)入下一個(gè)隊(duì)列去檢查。直到所有隊(duì)列檢查一遍,算一個(gè)輪詢。

  • 其中,timers、pending callbackidle prepare等執(zhí)行完畢后,到達(dá)poll隊(duì)列。

timers隊(duì)列的工作原理

timers并非真正意義上的隊(duì)列,他內(nèi)部存放的是計(jì)時(shí)器。
每次到達(dá)這個(gè)隊(duì)列,會(huì)檢查計(jì)時(shí)器線程內(nèi)的所有計(jì)時(shí)器,計(jì)時(shí)器線程內(nèi)部多個(gè)計(jì)時(shí)器按照時(shí)間順序排序。

檢查過程:將每一個(gè)計(jì)時(shí)器按順序分別計(jì)算一遍,計(jì)算該計(jì)時(shí)器開始計(jì)時(shí)的時(shí)間到當(dāng)前時(shí)間是否滿足計(jì)時(shí)器的間隔參數(shù)設(shè)定(比如1000ms,計(jì)算計(jì)時(shí)器開始計(jì)時(shí)到現(xiàn)在是否有1m)。當(dāng)某個(gè)計(jì)時(shí)器檢查通過,則執(zhí)行其回調(diào)函數(shù)。

poll隊(duì)列的運(yùn)作方式

  • 如果poll中有回調(diào)函數(shù)需要執(zhí)行,依次執(zhí)行回調(diào),直到清空隊(duì)列。

  • 如果poll中沒有回調(diào)函數(shù)需要執(zhí)行,已經(jīng)是空隊(duì)列了。則會(huì)在這里等待,等待其他隊(duì)列中出現(xiàn)回調(diào),

    • 如果其他隊(duì)列中出現(xiàn)回調(diào),則從poll向下到over,結(jié)束該階段,進(jìn)入下一階段。

    • 如果其他隊(duì)列也都沒有回調(diào),則持續(xù)在poll隊(duì)列等待,直到任何一個(gè)隊(duì)列出現(xiàn)回調(diào)后再進(jìn)行工作。(是個(gè)小懶蟲的處事方式)

舉例梳理事件流程

setTimeout(() => {
  console.log('object');
}, 5000)
console.log('node');
以上代碼的事件流程梳理
  • 進(jìn)入主線程,執(zhí)行setTimeout(),回調(diào)函數(shù)作為異步任務(wù)被放入異步隊(duì)列timers隊(duì)列中,暫時(shí)不執(zhí)行。

  • 繼續(xù)向下,執(zhí)行定時(shí)器后邊的console,打印“node”。

  • 判斷是否有事件循環(huán)。是,走一圈輪詢:從timers - pending callback - idle prepare……

  • poll隊(duì)列停下循環(huán)并等待。

    • 由于這時(shí)候沒到5秒,timers隊(duì)列無任務(wù),所以一直在poll隊(duì)列卡著,同時(shí)輪詢檢查其他隊(duì)列是否有任務(wù)。

  • 等5秒到達(dá),setTimeout的回調(diào)塞到timers內(nèi),例行輪詢檢查到timers隊(duì)列有任務(wù),則向下走,經(jīng)過check、close callbacks后到達(dá)timers。將timers隊(duì)列清空。

  • 繼續(xù)輪詢到poll等待,詢問是否還需要event loop,不需要,則到達(dá)over結(jié)束。

要理解這個(gè)問題,看下邊的代碼及流程解析:
setTimeout(function t1() {
  console.log('setTimeout');
}, 5000)
console.log('node 生命周期');

const http = require('http')

const server = http.createServer(function h2() {
  console.log('請(qǐng)求回調(diào)');
});

server.listen(8080)

代碼分析如下:

  • 照舊,先執(zhí)行主線程,打印“node 生命周期”、引入http后創(chuàng)建http服務(wù)。

  • 然后event loop檢查是否有異步任務(wù),檢查發(fā)現(xiàn)有定時(shí)器任務(wù)和請(qǐng)求任務(wù)。所以進(jìn)入事件循環(huán)。

  • 六個(gè)隊(duì)列都沒任務(wù),則在poll隊(duì)列等待。如下圖:

    如何理解Nodejs中的事件循環(huán)

  • 過了五秒,timers中有了任務(wù),則流程從poll放行向下,經(jīng)過check和close callbacks隊(duì)列后,到達(dá)event loop。

  • event loop檢查是否有異步任務(wù),檢查發(fā)現(xiàn)有定時(shí)器任務(wù)和請(qǐng)求任務(wù)。所以再次進(jìn)入事件循環(huán)。

  • 到達(dá)timers隊(duì)列,發(fā)現(xiàn)有回調(diào)函數(shù)任務(wù),則依次執(zhí)行回調(diào),清空timers隊(duì)列(當(dāng)然這里只有一個(gè)5秒到達(dá)后的回調(diào),所以直接執(zhí)行完了即可),打印出“setTimeout”。如下圖

    如何理解Nodejs中的事件循環(huán)

  • 清空timers隊(duì)列后,輪詢繼續(xù)向下到達(dá)poll隊(duì)列,由于poll隊(duì)列現(xiàn)在是空隊(duì)列,所以在這里等待。

  • 后來,假設(shè)用戶請(qǐng)求發(fā)來了,h2回調(diào)函數(shù)被放到poll隊(duì)列。于是poll中有回調(diào)函數(shù)需要執(zhí)行,依次執(zhí)行回調(diào),直到清空poll隊(duì)列。

  • poll隊(duì)列清空,此時(shí)poll隊(duì)列是空隊(duì)列,繼續(xù)等待。

    如何理解Nodejs中的事件循環(huán)

  • 由于node線程一直holding在poll隊(duì)列,等很長(zhǎng)一段時(shí)間還是沒有任務(wù)來臨時(shí),會(huì)自動(dòng)斷開等待(不自信表現(xiàn)),向下執(zhí)行輪詢流程,經(jīng)過check、close callbacks后到達(dá)event loop

  • 到了event loop后,檢查是否有異步任務(wù),檢查發(fā)現(xiàn)有請(qǐng)求任務(wù)。(此時(shí)定時(shí)器任務(wù)已經(jīng)執(zhí)行完畢,所以沒有了),則繼續(xù)再次進(jìn)入事件循環(huán)。

  • 到達(dá)poll隊(duì)列,再次holding……

  • 再等很長(zhǎng)時(shí)間沒有任務(wù)來臨,自動(dòng)斷開到even loop(再補(bǔ)充一點(diǎn)無任務(wù)的循環(huán)情況)

  • 再次回到poll隊(duì)列掛起

  • 無限循環(huán)……

梳理事件循環(huán)流程圖:

注意:下圖中的“是否有任務(wù)”的說法表示“是否有本隊(duì)列的任務(wù)”。

如何理解Nodejs中的事件循環(huán)

event loop流程梳理

再用一個(gè)典型的例子驗(yàn)證下流程:
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('結(jié)束死循環(huán)', new Date());
});

連續(xù)運(yùn)行三遍,打印結(jié)果如下:

如何理解Nodejs中的事件循環(huán)

執(zhí)行流程解析:

  • 執(zhí)行全局上下文,打印「node 生命周期 + 時(shí)間」

  • 詢問是否有event loop

  • 有,進(jìn)入timers隊(duì)列,檢查沒有計(jì)時(shí)器(cpu處理速度可以,這時(shí)還沒到200ms)

  • 輪詢進(jìn)入到poll,讀文件還沒讀完(比如此時(shí)才用了20ms),因此poll隊(duì)列是空的,也沒有任務(wù)回調(diào)

  • 在poll隊(duì)列等待……不斷輪詢看有沒有回調(diào)

  • 文件讀完,poll隊(duì)列有了fsFunc回調(diào)函數(shù),并且被執(zhí)行,輸出「fs + 時(shí)間」

  • 在while死循環(huán)那里卡300毫秒,

  • 死循環(huán)卡到200ms的時(shí)候,f1回調(diào)進(jìn)入timers隊(duì)列。但此時(shí)poll隊(duì)列很忙,占用了線程,不會(huì)向下執(zhí)行。

  • 直到300ms后poll隊(duì)列清空,輸出「結(jié)束死循環(huán) + 時(shí)間」

  • event loop趕緊向下走

  • 再來一輪到timers,執(zhí)行timers隊(duì)列里的f1回調(diào)。于是看到「setTimeout + 時(shí)間」

  • timers隊(duì)列清空,回到poll隊(duì)列,沒有任務(wù),等待一會(huì)。

  • 等待時(shí)間夠長(zhǎng)后,向下回到event loop。

  • event loop檢查沒有其他異步任務(wù)了,結(jié)束線程,整個(gè)程序over退出。

check 階段

檢查階段(使用 setImmediate 的回調(diào)會(huì)直接進(jìn)入這個(gè)隊(duì)列)

check隊(duì)列的實(shí)際工作原理

真正的隊(duì)列,里邊扔的就是待執(zhí)行的回調(diào)函數(shù)的集合。類似[fn,fn]這種形式的。
每次到達(dá)check這個(gè)隊(duì)列后,立即按順序執(zhí)行回調(diào)函數(shù)即可【類似于[fn1,fn2].forEach((fn)=>fn())的感覺】

所以說,setImmediate不是一個(gè)計(jì)時(shí)器的概念。

如果你去面試,涉及到Node環(huán)節(jié),可能會(huì)遇到下邊這個(gè)問題:setImmediate和setTimeout(0)誰(shuí)更快。

setImmediate() 與 setTimeout(0) 的對(duì)比

  • setImmediate的回調(diào)是異步的,和setTimeout回調(diào)性質(zhì)一致。

  • setImmediate回調(diào)在check隊(duì)列,setTimeout回調(diào)在timers隊(duì)列(概念意義,實(shí)際在計(jì)時(shí)器線程,只是setTimeout在timers隊(duì)列做檢查調(diào)用而已。詳細(xì)看timers的工作原理)。

  • setImmediate函數(shù)調(diào)用后,回調(diào)函數(shù)會(huì)立即push到check隊(duì)列,并在下次eventloop時(shí)被執(zhí)行。setTimeout函數(shù)調(diào)用后,計(jì)時(shí)器線程增加一個(gè)定時(shí)器任務(wù),下次eventloop時(shí)會(huì)在timers階段里檢查判斷定時(shí)器任務(wù)是否到達(dá)時(shí)間,到了則執(zhí)行回調(diào)函數(shù)。

  • 綜上,setImmediate的運(yùn)算速度比setTimeout(0)的要快,因?yàn)閟etTimeout還需要開計(jì)時(shí)器線程,并增加計(jì)算的開銷。

二者的效果差不多。但是執(zhí)行順序不定

觀察以下代碼:

setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

多次反復(fù)運(yùn)行,執(zhí)行效果如下:

如何理解Nodejs中的事件循環(huán)

順序不定

可以看到多次運(yùn)行,兩句console.log打印的順序不定。
這是因?yàn)閟etTimeout的間隔數(shù)最小填1,雖然下邊代碼填了0。但實(shí)際計(jì)算機(jī)執(zhí)行當(dāng)1ms算。(這里注意和瀏覽器的計(jì)時(shí)器區(qū)分。在瀏覽器中,setInterval的最小間隔數(shù)為10ms,小于10ms則會(huì)被設(shè)置為10;設(shè)備供電狀態(tài)下,間隔最小為16.6ms。)

以上代碼,主線程運(yùn)行的時(shí)候,setTimeout函數(shù)調(diào)用,計(jì)時(shí)器線程增加一個(gè)定時(shí)器任務(wù)。setImmediate函數(shù)調(diào)用后,其回調(diào)函數(shù)立即push到check隊(duì)列。主線程執(zhí)行完畢。

eventloop判斷時(shí),發(fā)現(xiàn)timers和check隊(duì)列有內(nèi)容,進(jìn)入異步輪詢:

第一種情況:等到了timers里這段時(shí)間,可能還沒有1ms的時(shí)間,定時(shí)器任務(wù)間隔時(shí)間的條件不成立所以timers里還沒有回調(diào)函數(shù)。繼續(xù)向下到了check隊(duì)列里,這時(shí)候setImmediate的回調(diào)函數(shù)早已等候多時(shí),直接執(zhí)行。而再下次eventloop到達(dá)timers隊(duì)列,定時(shí)器也早已成熟,才會(huì)執(zhí)行setTimeout的回調(diào)任務(wù)。于是順序就是「setImmediate -> setTimeout」。

第二種情況:但也有可能到了timers階段時(shí),超過了1ms。于是計(jì)算定時(shí)器條件成立,setTimeout的回調(diào)函數(shù)被直接執(zhí)行。eventloop再向下到達(dá)check隊(duì)列執(zhí)行setImmediate的回調(diào)。最終順序就是「setTimeout -> setImmediate」了。

所以,只比較這兩個(gè)函數(shù)的情況下,二者的執(zhí)行順序最終結(jié)果取決于當(dāng)下計(jì)算機(jī)的運(yùn)行環(huán)境以及運(yùn)行速度。

二者時(shí)間差距的對(duì)比代碼
------------------setTimeout測(cè)試:-------------------
let i = 0;
console.time('setTimeout');
function test() {
  if (i < 1000) {
    setTimeout(test, 0)
    i++
  } else {
    console.timeEnd('setTimeout');
  }
}
test();

------------------setImmediate測(cè)試:-------------------
let i = 0;
console.time('setImmediate');
function test() {
  if (i < 1000) {
    setImmediate(test)
    i++
  } else {
    console.timeEnd('setImmediate');
  }
}
test();

運(yùn)行觀察時(shí)間差距:

如何理解Nodejs中的事件循環(huán)

setTimeout與setImmediate時(shí)間差距

可見setTimeout遠(yuǎn)比setImmediate耗時(shí)多得多
這是因?yàn)閟etTimeout不僅有主代碼執(zhí)行的時(shí)間消耗。還有在timers隊(duì)列里,對(duì)于計(jì)時(shí)器線程中各個(gè)定時(shí)任務(wù)的計(jì)算時(shí)間。

結(jié)合poll隊(duì)列的面試題(考察timers、poll和check的執(zhí)行順序)

如果你看懂了上邊的事件循環(huán)圖,下邊這道題難不倒你!

// 說說下邊代碼的執(zhí)行順序,先打印哪個(gè)?
const fs = require('fs')
fs.readFile('./poll.js', () => {
  setTimeout(() => console.log('setTimeout'), 0)
  setImmediate(() => console.log('setImmediate'))
})

上邊這種代碼邏輯,不管執(zhí)行多少次,肯定都是先執(zhí)行setImmediate。

如何理解Nodejs中的事件循環(huán)

先執(zhí)行setImmediate

因?yàn)閒s各個(gè)函數(shù)的回調(diào)是放在poll隊(duì)列的。當(dāng)程序holding在poll隊(duì)列后,出現(xiàn)回調(diào)立即執(zhí)行。
回調(diào)內(nèi)執(zhí)行setTimeout和setImmediate的函數(shù)后,check隊(duì)列立即增加了回調(diào)。
回調(diào)執(zhí)行完畢,輪詢檢查其他隊(duì)列有內(nèi)容,程序結(jié)束poll隊(duì)列的holding向下執(zhí)行。
check是poll階段的緊接著的下一個(gè)。所以在向下的過程中,先執(zhí)行check階段內(nèi)的回調(diào),也就是先打印setImmediate。
到下一輪循環(huán),到達(dá)timers隊(duì)列,檢查setTimeout計(jì)時(shí)器符合條件,則定時(shí)器回調(diào)被執(zhí)行。

nextTick 與 Promise

說完宏任務(wù),接下來說下微任務(wù)

  • 二者都是「微隊(duì)列」,執(zhí)行異步微任務(wù)。

  • 二者不是事件循環(huán)的一部分,程序也不會(huì)開啟額外的線程去處理相關(guān)任務(wù)。(理解:promise里發(fā)網(wǎng)絡(luò)請(qǐng)求,那是網(wǎng)絡(luò)請(qǐng)求開的網(wǎng)絡(luò)線程,跟Promise這個(gè)微任務(wù)沒關(guān)系)

  • 微隊(duì)列設(shè)立的目的就是讓一些任務(wù)「馬上」、「立即」優(yōu)先執(zhí)行。

  • nextTick與Promise比較,nextTick的級(jí)別更高。

nextTick表現(xiàn)形式
process.nextTick(() => {})
Promise表現(xiàn)形式
Promise.resolve().then(() => {})
如何參與事件循環(huán)?

事件循環(huán)中,每執(zhí)行一個(gè)回調(diào)前,先按序清空一次nextTick和promise。

// 先思考下列代碼的執(zhí)行順序
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

兩個(gè)問題:

基于上邊的說法,有兩個(gè)問題待思考和解決:

  • 每走一個(gè)異步宏任務(wù)隊(duì)列就查一遍nextTick和promise?還是每執(zhí)行完 宏任務(wù)隊(duì)列里的一個(gè)回調(diào)函數(shù)就查一遍呢?

  • 如果在poll的holding階段,插入一個(gè)nextTick或者Promise的回調(diào),會(huì)立即停止poll隊(duì)列的holding去執(zhí)行回調(diào)嗎?

如何理解Nodejs中的事件循環(huán)

上邊兩個(gè)問題,看下邊代碼的說法

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');
  })
})

/** 執(zhí)行順序如下
global ------
nextTick 1
nextTick 2
promise 1
nextTick in promise
setTimeout 0 // 解釋問題1. 沒有上邊的nextTick和promise,setTimeout和setImmediate的順序不一定,有了以后肯定是0先開始。
// 可見,執(zhí)行一個(gè)隊(duì)列之前,就先檢查并執(zhí)行了nextTick和promise微隊(duì)列
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
 */

以上代碼執(zhí)行多次,順序不變,setTimeout和setImmediate的順序都沒變。

執(zhí)行順序及具體原因說明如下:

  • global :主線程同步任務(wù),率先執(zhí)行沒毛病

  • nextTick 1:執(zhí)行異步宏任務(wù)之前,清空異步微任務(wù),nextTick優(yōu)先級(jí)高,先行一步

  • nextTick 2:執(zhí)行完上邊這句代碼,又一個(gè)nextTick微任務(wù),立即率先執(zhí)行

  • promise 1:執(zhí)行異步宏任務(wù)之前,清空異步微任務(wù),Promise的優(yōu)先級(jí)低,所以在nextTick完了以后立即執(zhí)行

  • nextTick in promise:清空Promise隊(duì)列的過程中,遇到nextTick微任務(wù),立即執(zhí)行、清空

  • setTimeout 0: 解釋第一個(gè)問題. 沒有上邊的nextTick和promise,只有setTimeout和setImmediate時(shí)他倆的執(zhí)行順序不一定。有了以后肯定是0先開始??梢姡瑘?zhí)行一個(gè)宏隊(duì)列之前,就先按順序檢查并執(zhí)行了nextTick和promise微隊(duì)列。等微隊(duì)列全部執(zhí)行完畢,setTimeout(0)的時(shí)機(jī)也成熟了,就被執(zhí)行。

  • nextTick in setTimeout:執(zhí)行完上邊這句代碼,又一個(gè)nextTick微任務(wù),立即率先執(zhí)行 【這種回調(diào)函數(shù)里的微任務(wù),我不能確定是緊隨同步任務(wù)執(zhí)行的;還是放到微任務(wù)隊(duì)列,等下一個(gè)宏任務(wù)執(zhí)行前再清空的他們。但是順序看上去和立即執(zhí)行他們一樣。不過我比較傾向于是后者:先放到微任務(wù)隊(duì)列等待,下一個(gè)宏任務(wù)執(zhí)行前清空他們?!?/em>

  • setTimeout 1:因?yàn)閳?zhí)行微任務(wù)耗費(fèi)時(shí)間,導(dǎo)致此時(shí)timers里判斷兩個(gè)0和1的setTimeout計(jì)時(shí)器已經(jīng)結(jié)束,所以兩個(gè)setTimeout回調(diào)都已加入隊(duì)列并被執(zhí)行

  • nextTick in setTimeout1:執(zhí)行完上邊這句代碼,又一個(gè)nextTick微任務(wù),立即率先執(zhí)行 【可能是下一個(gè)宏任務(wù)前清空微任務(wù)】

  • promise in setTimeout1:執(zhí)行完上邊這句代碼,又一個(gè)Promise微任務(wù),立即緊隨執(zhí)行 【可能是下一個(gè)宏任務(wù)前清空微任務(wù)】

  • setImmediate:poll隊(duì)列回調(diào)時(shí)機(jī)未到,先行向下到check隊(duì)列,清空隊(duì)列,立即執(zhí)行setImmediate回調(diào)

  • nextTick in setImmediate:執(zhí)行完上邊這句代碼,又一個(gè)nextTick微任務(wù),立即率先執(zhí)行 【可能是下一個(gè)宏任務(wù)前清空微任務(wù)】

  • poll 1:poll隊(duì)列實(shí)際成熟,回調(diào)觸發(fā),同步任務(wù)執(zhí)行。

  • nextTick in poll :執(zhí)行完上邊這句代碼,又一個(gè)nextTick微任務(wù),立即率先執(zhí)行 【可能是下一個(gè)宏任務(wù)前清空微任務(wù)】

  • setTimeout 100:定時(shí)器任務(wù)到達(dá)時(shí)間,執(zhí)行回調(diào)。并在回調(diào)里往微任務(wù)推入了nextTick、Promise,往宏任務(wù)的check里推入了setImmediate的回調(diào)。并且也開啟了計(jì)時(shí)器線程,往timers里增加了下一輪回調(diào)的可能。

  • nextTick in setTimeout100:宏任務(wù)向下前,率先執(zhí)行定時(shí)器回調(diào)內(nèi)新增的微任務(wù)-nextTick 【這里就能確定了,是下一個(gè)宏任務(wù)前清空微任務(wù)的流程】

  • promise in setTimeout100:緊接著執(zhí)行定時(shí)器回調(diào)內(nèi)新增的微任務(wù)-Promise 【清空完nextTick清空Promise的順序】

  • setImmediate in setTimeout 100:這次setImmediate比setTimeout(0)先執(zhí)行的原因是:流程從timers向后走到check隊(duì)列,已經(jīng)有了setImmediate的回調(diào),立即執(zhí)行。

  • nextTick in setImmediate in setTimeout 100:執(zhí)行完上邊這句代碼,又一個(gè)nextTick微任務(wù),下一個(gè)宏任務(wù)前率先清空微任務(wù)

  • setTimeout 100 - 0:輪詢又一次回到timers,執(zhí)行100-0的回調(diào)。

  • nextTick in setTimeout 100 - 0:執(zhí)行完上邊這句代碼,又一個(gè)nextTick微任務(wù),下一個(gè)宏任務(wù)前率先清空微任務(wù)。

擴(kuò)展:為什么有了setImmediate還要有nextTick和Promise?

一開始設(shè)計(jì)的時(shí)候,setImmediate充當(dāng)了微隊(duì)列的作用(雖然他不是)。設(shè)計(jì)者希望執(zhí)行完poll后立即執(zhí)行setImmediate(當(dāng)然現(xiàn)在也確實(shí)是這么表現(xiàn)的)。所以起的名字叫Immediate,表示立即的意思。 但是后來問題是,poll里可能有N個(gè)任務(wù)連續(xù)執(zhí)行,在執(zhí)行期間想要執(zhí)行setImmediate是不可能的。因?yàn)閜oll隊(duì)列不停,流程不向下執(zhí)行。

于是出現(xiàn)nextTick,真正的微隊(duì)列概念。但此時(shí),immediate的名字被占用了,所以名字叫nextTick(下一瞬間)。事件循環(huán)期間,執(zhí)行任何一個(gè)隊(duì)列之前,都要檢查他是否被清空。其次是Promise。

面試題

最后,檢驗(yàn)學(xué)習(xí)成果的面試題來了

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

// 后邊這仨的運(yùn)行順序就是驗(yàn)證你電腦運(yùn)算速度的時(shí)候了。
速度最好(執(zhí)行上邊的同步代碼 + 微任務(wù) + 計(jì)時(shí)器運(yùn)算用了不到0ms):
setImmediate
setTimeout 0
setTimeout 3

速度中等(執(zhí)行上邊的同步代碼 + 微任務(wù) + 計(jì)時(shí)器運(yùn)算用了0~3ms以上):
setTimeout 0
setImmediate
setTimeout 3

速度較差(執(zhí)行上邊的同步代碼 + 微任務(wù) + 計(jì)時(shí)器運(yùn)算用了3ms以上):
setTimeout 0
setTimeout 3
setImmediate
*/

思維腦圖 - Node生命周期核心階段

如何理解Nodejs中的事件循環(huán)

上述就是小編為大家分享的如何理解Nodejs中的事件循環(huán)了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI