什么是事件輪詢
大家都知道咱台, JavaScript是單線程的络拌, 那么nodejs是如何做到非阻塞呢,在nodejs內(nèi)部使用了第三方庫(kù)libuv回溺,nodejs會(huì)把IO春贸,文件讀取等異步操作交由他處理,而nodejs主線程可以繼續(xù)去處理其他的事情遗遵。libuv會(huì)開(kāi)啟不同的線程去處理這些延時(shí)操作萍恕,處理完后,會(huì)把異步操作的回調(diào)函數(shù)放到nodejs的輪詢隊(duì)列中车要,nodejs會(huì)在適當(dāng)?shù)臅r(shí)候處理輪詢隊(duì)列中的回調(diào)函數(shù)允粤,從而實(shí)現(xiàn)非阻塞。所以,實(shí)際上nodejs在處理這些阻塞操作時(shí)类垫,并不是單線程的司光。
事件輪詢?cè)斀?/h2>
下圖是nodejs官網(wǎng)的事件輪詢流程圖
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
從圖中可以看出, 事件輪詢機(jī)制分為六個(gè)階段悉患,每個(gè)階段都有一個(gè) FIFO 隊(duì)列來(lái)執(zhí)行回調(diào)残家。雖然每個(gè)階段都是特殊的,但通常情況下售躁,當(dāng)事件循環(huán)進(jìn)入給定的階段時(shí)坞淮,它將執(zhí)行特定于該階段的任何操作,然后在該階段的隊(duì)列中執(zhí)行回調(diào)陪捷,直到隊(duì)列用盡或最大回調(diào)數(shù)已執(zhí)行回窘。當(dāng)該隊(duì)列已用盡或達(dá)到回調(diào)限制,事件循環(huán)將移動(dòng)到下一階段揩局。
事件輪詢階段詳解
timers階段
概述: timers階段用來(lái)處理setTimeout() 和 setInterval() 的回調(diào)函數(shù)毫玖。
詳解 :我們都使用過(guò)setTimeout(callback, delay), setInterval(callback, delay), 第一個(gè)參數(shù)是回調(diào)函數(shù)掀虎, 第二個(gè)參數(shù)是延遲時(shí)間(ms)凌盯,即多久之后執(zhí)行。但是在絕大多數(shù)情況下烹玉,它并不會(huì)絕對(duì)守時(shí)驰怎。因?yàn)椴僮飨到y(tǒng)調(diào)度或其它回調(diào)的運(yùn)行可能會(huì)延遲它們。比如說(shuō)二打, 當(dāng)輪詢進(jìn)入到poll階段的時(shí)候并且poll階段的回調(diào)隊(duì)列不為空县忌, 如果timers此時(shí)已經(jīng)有一個(gè)setTimeout達(dá)到了預(yù)設(shè)的延時(shí)時(shí)間, 系統(tǒng)也需要先處理完poll階段的回調(diào)隊(duì)列继效,才能去處理timers的回調(diào)隊(duì)列症杏。
pending callbacks階段
概述: 這個(gè)階段用來(lái)處理系統(tǒng)操作的回調(diào)函數(shù)
詳解 :此階段對(duì)某些系統(tǒng)操作(如 TCP 錯(cuò)誤類型)執(zhí)行回調(diào)。例如瑞信,如果 TCP 套接字在嘗試連接時(shí)接收到 ECONNREFUSED厉颤,這些錯(cuò)誤的回調(diào)將被放到此階段的回調(diào)函數(shù)。
idle prepare階段
概述: 此階段是僅供nodejs內(nèi)部操作調(diào)用凡简,我們不必討論
poll階段
概述: 這個(gè)階段主要用來(lái)處理如IO操作逼友,網(wǎng)絡(luò)請(qǐng)求等異步操作
詳解 :這各階段會(huì)有不同的情況
1.當(dāng)poll階段的回調(diào)函數(shù)隊(duì)列不為空的時(shí)候,則處理隊(duì)列中的回調(diào)函數(shù)秤涩,直到隊(duì)列為空或者達(dá)到系統(tǒng)處理的上限的時(shí)候帜乞,就跳過(guò)此階段,處理下一階段筐眷。
2.當(dāng)進(jìn)入poll階段的時(shí)候黎烈,如果此階段的回調(diào)隊(duì)列為空,系統(tǒng)會(huì)在此階段等待新的回調(diào)函數(shù)入隊(duì),再進(jìn)行處理照棋。如果一直等不到新的回調(diào)函數(shù)呢津畸?咋辦?阻塞在這里必怜?一直等肉拓?不會(huì)的,在這個(gè)階段會(huì)同時(shí)進(jìn)行檢測(cè)timers階段是否已經(jīng)有回調(diào)函數(shù)超時(shí)梳庆,如果有暖途,則馬上跳過(guò)poll階段,進(jìn)入下一個(gè)階段膏执。
那么在poll階段是如何利用libuv庫(kù)來(lái)處理io及文件讀取等操作的呢驻售?看下圖
從圖中可以看得出,libuv自身有一個(gè)EventQueue的隊(duì)列更米,這個(gè)隊(duì)列里面都是一些如文件讀取欺栗,網(wǎng)絡(luò)請(qǐng)求的操作,libuv依次去處理這個(gè)隊(duì)列中的操作征峦,libuv每當(dāng)從EventQueue中拿到一個(gè)處理事件迟几, 就會(huì)分配一個(gè)線程給它,讓這個(gè)線程去處理這個(gè)事件栏笆,當(dāng)這個(gè)線程處理完畢這個(gè)事件的時(shí)候类腮,就會(huì)將結(jié)果返回到EventQueue隊(duì)列, 當(dāng)再次獲取到該事件的時(shí)候蛉加, 發(fā)現(xiàn)已經(jīng)不是IO操作了蚜枢,就會(huì)把這個(gè)事件的回調(diào)函數(shù)放到poll階段的隊(duì)列中,再交由poll去處理回調(diào)函數(shù)针饥。所以EventQueue在每次出隊(duì)的時(shí)候都會(huì)進(jìn)行判斷該操作是否是IO操作厂抽,不是的話就直接返回給poll階段的隊(duì)列了。需要注意的是丁眼,libuv默認(rèn)只開(kāi)啟了4個(gè)線程筷凤,你可以通過(guò)設(shè)置環(huán)境變量來(lái)修改線程數(shù)量。
check階段
概述: 這個(gè)階段用來(lái)處理setImmediate的回調(diào)函數(shù)
詳解 :當(dāng)poll階段的回調(diào)隊(duì)列為空的時(shí)候(或者達(dá)到系統(tǒng)執(zhí)行的上限)户盯,就會(huì)進(jìn)入到check階段來(lái)處理setImmediate的回調(diào)函數(shù)嵌施。
close callbacks階段
概述: 這個(gè)階段用來(lái)處理如socket的close事件
詳解 :顧名思義, 關(guān)閉回調(diào)函數(shù)莽鸭, 如socket.on("close", () => {...})
process.nextTick及Promise
上面的討論一直未提及process.nextTick和Promise的執(zhí)行吗伤,原因是這兩個(gè)函數(shù)比較特殊,它們不由libuv去管理硫眨,而且它們的優(yōu)先級(jí)要高于事件輪詢的每一個(gè)階段足淆。它們會(huì)在事件輪詢的每一個(gè)階段之間執(zhí)行,注意是事件輪詢的每一個(gè)階段之間。process.nextTick會(huì)放在nextTickQueue巧号, Promise會(huì)放在microTaskQueue族奢,在每次事件輪詢進(jìn)入到下一個(gè)階段的時(shí)候, 都會(huì)檢查這兩個(gè)隊(duì)列是否為空丹鸿,不為空則馬上處理它們的回調(diào)越走。
注意因?yàn)閜rocess.nextTick會(huì)在事件輪詢每個(gè)階段之間執(zhí)行, 如果遞歸調(diào)用nextTick靠欢, 就會(huì)導(dǎo)致輪詢阻塞廊敌,所以盡量避免使用process.nextTick, 可以使用setImmediate代替门怪。
有趣的事情
從process.nextTick和setImmediate的名字上來(lái)看骡澈,setImmediate應(yīng)當(dāng)是要先于process.nextTick執(zhí)行的。但事實(shí)恰好相反掷空,nodejs官網(wǎng)也給出了解釋肋殴,這是nodejs歷史原因,很難再去修改他們的名字L沟堋护锤!??????