摘要:?jiǎn)栴}引入接觸過(guò)事件循環(huán)的同學(xué)大都會(huì)糾結(jié)一個(gè)點(diǎn),就是在中和執(zhí)行順序的隨機(jī)性。當(dāng)隊(duì)列被執(zhí)行完,或者執(zhí)行的回調(diào)數(shù)量達(dá)到上限后,事件循環(huán)才會(huì)進(jìn)入下一個(gè)階段。嵌套的在下一個(gè)事件循環(huán)的階段執(zhí)行回調(diào)輸出嵌套的。
問(wèn)題引入
接觸過(guò)事件循環(huán)的同學(xué)大都會(huì)糾結(jié)一個(gè)點(diǎn),就是在Node中setTimeout和setImmediate執(zhí)行順序的隨機(jī)性。
比如說(shuō)下面這段代碼:
setTimeout(() => { console.log("setTimeout"); }, 0); setImmediate(() => { console.log("setImmediate"); })
執(zhí)行的結(jié)果是這樣子的:
為什么會(huì)出現(xiàn)這種情況呢?別急,我們先往下看。
瀏覽器中事件循環(huán)模型我們都知道,JavaScript是單線程的語(yǔ)言,對(duì)I/O的控制是通過(guò)異步來(lái)實(shí)現(xiàn)的,具體是通過(guò)“事件循環(huán)”機(jī)制來(lái)實(shí)現(xiàn)。
對(duì)于JavaScript中的單線程,指的是JavaScript執(zhí)行在單線程中,而內(nèi)部I/O任務(wù)其實(shí)是另有線程池來(lái)完成的。
在瀏覽器中,我們討論事件循環(huán),是以“從宏任務(wù)隊(duì)列中取一個(gè)任務(wù)執(zhí)行,再取出微任務(wù)隊(duì)列中的所有任務(wù)”來(lái)分析執(zhí)行代碼的。但是在Node環(huán)境中并不適用。具體的瀏覽器事件循環(huán)解析:傳送門
在Node中,事件循環(huán)的模型和瀏覽器相比大致相同,而最大的不同點(diǎn)在于Node中事件循環(huán)分不同的階段。具體我們下面會(huì)討論到。本文核心也在這里。
Node中事件循環(huán)階段解析下面是事件循環(huán)不同階段的示意圖:
每個(gè)階段都有一個(gè)先進(jìn)先出的回調(diào)隊(duì)列要執(zhí)行。而每個(gè)階段都有自己的特殊之處。簡(jiǎn)單來(lái)說(shuō),就是當(dāng)事件循環(huán)進(jìn)入某個(gè)階段后,會(huì)執(zhí)行該階段特定的任意操作,然后才會(huì)執(zhí)行這個(gè)階段里的回調(diào)。當(dāng)隊(duì)列被執(zhí)行完,或者執(zhí)行的回調(diào)數(shù)量達(dá)到上限后,事件循環(huán)才會(huì)進(jìn)入下一個(gè)階段。
以下是各個(gè)階段詳情。
timers一個(gè)timer指定一個(gè)下限時(shí)間而不是準(zhǔn)確時(shí)間,在達(dá)到這個(gè)下限時(shí)間后執(zhí)行回調(diào)。在指定的時(shí)間過(guò)后,timers會(huì)盡早的執(zhí)行回調(diào),但是系統(tǒng)調(diào)度或者其他回調(diào)的執(zhí)行可能會(huì)延遲它們。
從技術(shù)上來(lái)說(shuō),poll階段控制timers什么時(shí)候執(zhí)行,而執(zhí)行的具體位置在timers。
下限的時(shí)間有一個(gè)范圍:[1, 2147483647],如果設(shè)定的時(shí)間不在這個(gè)范圍,將被設(shè)置為1。
I/O callbacks這個(gè)階段執(zhí)行一些系統(tǒng)操作的回調(diào),比如說(shuō)TCP連接發(fā)生錯(cuò)誤。
idle, prepare系統(tǒng)內(nèi)部的一些調(diào)用。
poll這是最復(fù)雜的一個(gè)階段。
poll階段有兩個(gè)主要的功能:一是執(zhí)行下限時(shí)間已經(jīng)達(dá)到的timers的回調(diào),一是處理poll隊(duì)列里的事件。
注:Node很多API都是基于事件訂閱完成的,這些API的回調(diào)應(yīng)該都在poll階段完成。
以下是Node官網(wǎng)的介紹:
筆者把官網(wǎng)陳述的情況以不同的條件分解,更加的清楚。(如果有誤,師請(qǐng)改正。)
當(dāng)事件循環(huán)進(jìn)入poll階段:
poll隊(duì)列不為空的時(shí)候,事件循環(huán)肯定是先遍歷隊(duì)列并同步執(zhí)行回調(diào),直到隊(duì)列清空或執(zhí)行回調(diào)數(shù)達(dá)到系統(tǒng)上限。
poll隊(duì)列為空的時(shí)候,這里有兩種情況。
如果代碼已經(jīng)被setImmediate()設(shè)定了回調(diào),那么事件循環(huán)直接結(jié)束poll階段進(jìn)入check階段來(lái)執(zhí)行check隊(duì)列里的回調(diào)。
如果代碼沒有被設(shè)定setImmediate()設(shè)定回調(diào):
如果有被設(shè)定的timers,那么此時(shí)事件循環(huán)會(huì)檢查timers,如果有一個(gè)或多個(gè)timers下限時(shí)間已經(jīng)到達(dá),那么事件循環(huán)將繞回timers階段,并執(zhí)行timers的有效回調(diào)隊(duì)列。
如果沒有被設(shè)定timers,這個(gè)時(shí)候事件循環(huán)是阻塞在poll階段等待回調(diào)被加入poll隊(duì)列。
check這個(gè)階段允許在poll階段結(jié)束后立即執(zhí)行回調(diào)。如果poll階段空閑,并且有被setImmediate()設(shè)定的回調(diào),那么事件循環(huán)直接跳到check執(zhí)行而不是阻塞在poll階段等待回調(diào)被加入。
setImmediate()實(shí)際上是一個(gè)特殊的timer,跑在事件循環(huán)中的一個(gè)獨(dú)立的階段。它使用libuv的API來(lái)設(shè)定在poll階段結(jié)束后立即執(zhí)行回調(diào)。
注:setImmediate()具有最高優(yōu)先級(jí),只要poll隊(duì)列為空,代碼被setImmediate(),無(wú)論是否有timers達(dá)到下限時(shí)間,setImmediate()的代碼都先執(zhí)行。
close callbacks如果一個(gè)socket或handle被突然關(guān)掉(比如socket.destroy()),close事件將在這個(gè)階段被觸發(fā),否則將通過(guò)process.nextTick()觸發(fā)。
關(guān)于setTimeout和setImmediate代碼重現(xiàn),我們會(huì)發(fā)現(xiàn)setTimeout和setImmediate在Node環(huán)境下執(zhí)行是靠“隨緣法則”的。
比如說(shuō)下面這段代碼:
setTimeout(() => { console.log("setTimeout"); }, 0); setImmediate(() => { console.log("setImmediate"); })
執(zhí)行的結(jié)果是這樣子的:
為什么會(huì)這樣子呢?
這里我們要根據(jù)前面的那個(gè)事件循環(huán)不同階段的圖解來(lái)說(shuō)明一下:
首先進(jìn)入的是timers階段,如果我們的機(jī)器性能一般,那么進(jìn)入timers階段,一毫秒已經(jīng)過(guò)去了(setTimeout(fn, 0)等價(jià)于setTimeout(fn, 1)),那么setTimeout的回調(diào)會(huì)首先執(zhí)行。
如果沒有到一毫秒,那么在timers階段的時(shí)候,下限時(shí)間沒到,setTimeout回調(diào)不執(zhí)行,事件循環(huán)來(lái)到了poll階段,這個(gè)時(shí)候隊(duì)列為空,此時(shí)有代碼被setImmediate(),于是先執(zhí)行了setImmediate()的回調(diào)函數(shù),之后在下一個(gè)事件循環(huán)再執(zhí)行setTimemout的回調(diào)函數(shù)。
而我們?cè)趫?zhí)行代碼的時(shí)候,進(jìn)入timers的時(shí)間延遲其實(shí)是隨機(jī)的,并不是確定的,所以會(huì)出現(xiàn)兩個(gè)函數(shù)執(zhí)行順序隨機(jī)的情況。
那我們?cè)賮?lái)看一段代碼:
var fs = require("fs") fs.readFile(__filename, () => { setTimeout(() => { console.log("timeout"); }, 0); setImmediate(() => { console.log("immediate"); }); });
這里我們就會(huì)發(fā)現(xiàn),setImmediate永遠(yuǎn)先于setTimeout執(zhí)行。
原因如下:
fs.readFile的回調(diào)是在poll階段執(zhí)行的,當(dāng)其回調(diào)執(zhí)行完畢之后,poll隊(duì)列為空,而setTimeout入了timers的隊(duì)列,此時(shí)有代碼被setImmediate(),于是事件循環(huán)先進(jìn)入check階段執(zhí)行回調(diào),之后在下一個(gè)事件循環(huán)再在timers階段中執(zhí)行有效回調(diào)。
同樣的,這段代碼也是一樣的道理:
setTimeout(() => { setImmediate(() => { console.log("setImmediate"); }); setTimeout(() => { console.log("setTimeout"); }, 0); }, 0);
以上的代碼在timers階段執(zhí)行外部的setTimeout回調(diào)后,內(nèi)層的setTimeout和setImmediate入隊(duì),之后事件循環(huán)繼續(xù)往后面的階段走,走到poll階段的時(shí)候發(fā)現(xiàn)隊(duì)列為空,此時(shí)有代碼被setImmedate(),所以直接進(jìn)入check階段執(zhí)行響應(yīng)回調(diào)(注意這里沒有去檢測(cè)timers隊(duì)列中是否有成員到達(dá)下限事件,因?yàn)?b>setImmediate()優(yōu)先)。之后在第二個(gè)事件循環(huán)的timers階段中再去執(zhí)行相應(yīng)的回調(diào)。
綜上,我們可以總結(jié):
如果兩者都在主模塊中調(diào)用,那么執(zhí)行先后取決于進(jìn)程性能,也就是隨機(jī)。
如果兩者都不在主模塊調(diào)用(被一個(gè)異步操作包裹),那么setImmediate的回調(diào)永遠(yuǎn)先執(zhí)行。
process.nextTick() and Promise對(duì)于這兩個(gè),我們可以把它們理解成一個(gè)微任務(wù)。也就是說(shuō),它其實(shí)不屬于事件循環(huán)的一部分。
那么他們是在什么時(shí)候執(zhí)行呢?
不管在什么地方調(diào)用,他們都會(huì)在其所處的事件循環(huán)最后,事件循環(huán)進(jìn)入下一個(gè)循環(huán)的階段前執(zhí)行。
舉個(gè)?:
setTimeout(() => { console.log("timeout0"); process.nextTick(() => { console.log("nextTick1"); process.nextTick(() => { console.log("nextTick2"); }); }); process.nextTick(() => { console.log("nextTick3"); }); console.log("sync"); setTimeout(() => { console.log("timeout2"); }, 0); }, 0);
結(jié)果是:
再解釋一下:
timers階段執(zhí)行外層setTimeout的回調(diào),遇到同步代碼先執(zhí)行,也就有timeout0、sync的輸出。遇到process.nextTick后入微任務(wù)隊(duì)列,依次nextTick1、nextTick3、nextTick2入隊(duì)后出隊(duì)輸出。之后,在下一個(gè)事件循環(huán)的timers階段,執(zhí)行setTimeout回調(diào)輸出timeout2。
最后下面給出兩段代碼,如果能夠理解其執(zhí)行順序說(shuō)明你已經(jīng)理解透徹。
代碼1:
setImmediate(function(){ console.log("setImmediate"); setImmediate(function(){ console.log("嵌套setImmediate"); }); process.nextTick(function(){ console.log("nextTick"); }) }); // setImmediate // nextTick // 嵌套setImmediate
解析:事件循環(huán)check階段執(zhí)行回調(diào)函數(shù)輸出setImmediate,之后輸出nextTick。嵌套的setImmediate在下一個(gè)事件循環(huán)的check階段執(zhí)行回調(diào)輸出嵌套的setImmediate。
代碼2:
var fs = require("fs"); function someAsyncOperation (callback) { // 假設(shè)這個(gè)任務(wù)要消耗 95ms fs.readFile("/path/to/file", callback); } var timeoutScheduled = Date.now(); setTimeout(function () { var delay = Date.now() - timeoutScheduled; console.log(delay + "ms have passed since I was scheduled"); }, 100); // someAsyncOperation要消耗 95 ms 才能完成 someAsyncOperation(function () { var startCallback = Date.now(); // 消耗 10ms... while (Date.now() - startCallback < 10) { ; // do nothing } });
解析:事件循環(huán)進(jìn)入poll階段發(fā)現(xiàn)隊(duì)列為空,并且沒有代碼被setImmediate()。于是在poll階段等待timers下限時(shí)間到達(dá)。當(dāng)?shù)鹊?b>95ms時(shí),fs.readFile首先執(zhí)行了,它的回調(diào)被添加進(jìn)poll隊(duì)列并同步執(zhí)行,耗時(shí)10ms。此時(shí)總共時(shí)間累積105ms。等到poll隊(duì)列為空的時(shí)候,事件循環(huán)會(huì)查看最近到達(dá)的timer的下限時(shí)間,發(fā)現(xiàn)已經(jīng)到達(dá),再回到timers階段,執(zhí)行timer的回調(diào)。
如果有什么問(wèn)題,歡迎留言交流探討。
參考鏈接:
https://nodejs.org/en/docs/gu...
https://github.com/creeperyan...
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/107169.html
摘要:異步在中,是在單線程中執(zhí)行的沒錯(cuò),但是內(nèi)部完成工作的另有線程池,使用一個(gè)主進(jìn)程和多個(gè)線程來(lái)模擬異步。在事件循環(huán)中,觀察者會(huì)不斷的找到線程池中已經(jīng)完成的請(qǐng)求對(duì)象,從中取出回調(diào)函數(shù)和數(shù)據(jù)并執(zhí)行。 1. 介紹 單線程編程會(huì)因阻塞I/O導(dǎo)致硬件資源得不到更優(yōu)的使用。多線程編程也因?yàn)榫幊讨械乃梨i、狀態(tài)同步等問(wèn)題讓開發(fā)人員頭痛。Node在兩者之間給出了它的解決方案:利用單線程,遠(yuǎn)離多線程死鎖、狀態(tài)...
摘要:概述本篇主要介紹的運(yùn)行機(jī)制單線程事件循環(huán)結(jié)論先在中利用運(yùn)行至完成和非阻塞完成單線程下異步任務(wù)的處理就是先處理主模塊主線程上的同步任務(wù)再處理異步任務(wù)異步任務(wù)使用事件循環(huán)機(jī)制完成調(diào)度涉及的內(nèi)容有單線程事件循環(huán)同步執(zhí)行異步執(zhí)行定時(shí)器的事件循環(huán)開始 1.概述 本篇主要介紹JavaScript的運(yùn)行機(jī)制:單線程事件循環(huán)(Event Loop). 結(jié)論先: 在JavaScript中, 利用運(yùn)行至...
摘要:事件觸發(fā)線程主要負(fù)責(zé)將準(zhǔn)備好的事件交給引擎線程執(zhí)行。它將不同的任務(wù)分配給不同的線程,形成一個(gè)事件循環(huán),以異步的方式將任務(wù)的執(zhí)行結(jié)果返回給引擎。 Fundebug經(jīng)作者浪里行舟授權(quán)首發(fā),未經(jīng)同意請(qǐng)勿轉(zhuǎn)載。 前言 本文我們將會(huì)介紹 JS 實(shí)現(xiàn)異步的原理,并且了解了在瀏覽器和 Node 中 Event Loop 其實(shí)是不相同的。 一、線程與進(jìn)程 1. 概念 我們經(jīng)常說(shuō) JS 是單線程執(zhí)行的,...
本文涵蓋 面試題的引入 對(duì)事件循環(huán)面試題執(zhí)行順序的一些疑問(wèn) 通過(guò)面試題對(duì)微任務(wù)、事件循環(huán)、定時(shí)器等對(duì)深入理解 結(jié)論總結(jié) 面試題 面試題如下,大家可以先試著寫一下輸出結(jié)果,然后再看我下面的詳細(xì)講解,看看會(huì)不會(huì)有什么出入,如果把整個(gè)順序弄清楚 Node.js 的執(zhí)行順序應(yīng)該就沒問(wèn)題了。 async function async1(){ console.log(async1 start) ...
閱讀 3021·2021-11-23 09:51
閱讀 3624·2021-10-13 09:39
閱讀 2511·2021-09-22 15:06
閱讀 894·2019-08-30 15:55
閱讀 3164·2019-08-30 15:44
閱讀 1793·2019-08-30 14:05
閱讀 3448·2019-08-29 15:24
閱讀 2373·2019-08-29 12:44