瀏覽器與Node的事件循環(huán)(Event Loop)有何區(qū)別?

瀏覽器中的 Event Loop

1谓着、Micro-Task 與 Macro-Task

事件循環(huán)中的異步隊(duì)列有兩種:macro(宏任務(wù))隊(duì)列和 micro(微任務(wù))隊(duì)列。宏任務(wù)隊(duì)列可以有多個(gè),微任務(wù)隊(duì)列只有一個(gè)源祈。

常見的 macro-task 比如:setTimeout榆苞、setInterval猛拴、 setImmediate、script(整體代碼)蚀狰、 I/O 操作愉昆、UI 渲染等。

常見的 micro-task 比如: process.nextTick麻蹋、new Promise().then(回調(diào))跛溉、MutationObserver(html5 新特性) 等。

當(dāng)某個(gè)宏任務(wù)執(zhí)行完后,會(huì)查看是否有微任務(wù)隊(duì)列扮授。如果有芳室,先執(zhí)行微任務(wù)隊(duì)列中的所有任務(wù),如果沒有刹勃,會(huì)讀取宏任務(wù)隊(duì)列中排在最前的任務(wù)堪侯,執(zhí)行宏任務(wù)的過程中,遇到微任務(wù)深夯,依次加入微任務(wù)隊(duì)列抖格。棧空后咕晋,再次讀取微任務(wù)隊(duì)列里的任務(wù)雹拄,依次類推。

接下來我們看道例子來介紹上面流程:

```????Promise.resolve().then(()=>{

? console.log('Promise1')

? setTimeout(()=>{

? ? console.log('setTimeout2')

? },0)

})setTimeout(()=>{

? console.log('setTimeout1')

? Promise.resolve().then(()=>{

? ? console.log('Promise2')

? })

},0)

```

最后輸出結(jié)果是 Promise1掌呜,setTimeout1滓玖,Promise2,setTimeout2

一開始執(zhí)行棧的同步任務(wù)(這屬于宏任務(wù))執(zhí)行完畢质蕉,會(huì)去查看是否有微任務(wù)隊(duì)列势篡,上題中存在(有且只有一個(gè)),然后執(zhí)行微任務(wù)隊(duì)列中的所有任務(wù)輸出 Promise1模暗,同時(shí)會(huì)生成一個(gè)宏任務(wù) setTimeout2

然后去查看宏任務(wù)隊(duì)列禁悠,宏任務(wù) setTimeout1 在 setTimeout2 之前,先執(zhí)行宏任務(wù) setTimeout1兑宇,輸出 setTimeout1

在執(zhí)行宏任務(wù) setTimeout1 時(shí)會(huì)生成微任務(wù) Promise2 碍侦,放入微任務(wù)隊(duì)列中,接著先去清空微任務(wù)隊(duì)列中的所有任務(wù)隶糕,輸出 Promise2

清空完微任務(wù)隊(duì)列中的所有任務(wù)后瓷产,就又會(huì)去宏任務(wù)隊(duì)列取一個(gè),這回執(zhí)行的是 setTimeout2

Node 中的 Event Loop


1. Node 簡(jiǎn)介

Node 中的 Event Loop 和瀏覽器中的是完全不相同的東西枚驻。Node.js 采用 V8 作為 js 的解析引擎濒旦,而 I/O 處理方面使用了自己設(shè)計(jì)的 libuv,libuv 是一個(gè)基于事件驅(qū)動(dòng)的跨平臺(tái)抽象層再登,封裝了不同操作系統(tǒng)一些底層特性尔邓,對(duì)外提供統(tǒng)一的 API晾剖,事件循環(huán)機(jī)制也是它里面的實(shí)現(xiàn)(下文會(huì)詳細(xì)介紹)。

Node.js 的運(yùn)行機(jī)制如下:

V8 引擎解析 JavaScript 腳本铃拇。

解析后的代碼钞瀑,調(diào)用 Node API。

libuv 庫負(fù)責(zé) Node API 的執(zhí)行慷荔。它將不同的任務(wù)分配給不同的線程,形成一個(gè) Event Loop(事件循環(huán))缠俺,以異步的方式將任務(wù)的執(zhí)行結(jié)果返回給 V8 引擎显晶。

V8 引擎再將結(jié)果返回給用戶。

2. 六個(gè)階段

其中 libuv 引擎中的事件循環(huán)分為 6 個(gè)階段壹士,它們會(huì)按照順序反復(fù)運(yùn)行磷雇。每當(dāng)進(jìn)入某一個(gè)階段的時(shí)候,都會(huì)從對(duì)應(yīng)的回調(diào)隊(duì)列中取出函數(shù)去執(zhí)行躏救。當(dāng)隊(duì)列為空或者執(zhí)行的回調(diào)函數(shù)數(shù)量到達(dá)系統(tǒng)設(shè)定的閾值唯笙,就會(huì)進(jìn)入下一階段。


圖1

從上圖中盒使,大致看出 node 中的事件循環(huán)的順序:

外部輸入數(shù)據(jù)-->輪詢階段(poll)-->檢查階段(check)-->關(guān)閉事件回調(diào)階段(close callback)-->定時(shí)器檢測(cè)階段(timer)-->I/O 事件回調(diào)階段(I/O callbacks)-->閑置階段(idle, prepare)-->輪詢階段(按照該順序反復(fù)運(yùn)行)...

timers 階段:這個(gè)階段執(zhí)行 timer(setTimeout崩掘、setInterval)的回調(diào)

I/O callbacks 階段:處理一些上一輪循環(huán)中的少數(shù)未執(zhí)行的 I/O 回調(diào)

idle, prepare 階段:僅 node 內(nèi)部使用

poll 階段:獲取新的 I/O 事件, 適當(dāng)?shù)臈l件下 node 將阻塞在這里

check 階段:執(zhí)行 setImmediate() 的回調(diào)

close callbacks 階段:執(zhí)行 socket 的 close 事件回調(diào)

注意:上面六個(gè)階段都不包括 process.nextTick()(下文會(huì)介紹)

接下去我們?cè)敿?xì)介紹timers、poll少办、check這 3 個(gè)階段苞慢,因?yàn)槿粘i_發(fā)中的絕大部分異步任務(wù)都是在這 3 個(gè)階段處理的。

(1) timer

timers 階段會(huì)執(zhí)行 setTimeout 和 setInterval 回調(diào)英妓,并且是由 poll 階段控制的挽放。

同樣,在 Node 中定時(shí)器指定的時(shí)間也不是準(zhǔn)確時(shí)間蔓纠,只能是盡快執(zhí)行辑畦。

(2) poll

poll 是一個(gè)至關(guān)重要的階段,這一階段中腿倚,系統(tǒng)會(huì)做兩件事情

回到 timer 階段執(zhí)行回調(diào)

執(zhí)行 I/O 回調(diào)

并且在進(jìn)入該階段時(shí)如果沒有設(shè)定了 timer 的話纯出,會(huì)發(fā)生以下兩件事情

如果 poll 隊(duì)列不為空,會(huì)遍歷回調(diào)隊(duì)列并同步執(zhí)行猴誊,直到隊(duì)列為空或者達(dá)到系統(tǒng)限制

如果 poll 隊(duì)列為空時(shí)潦刃,會(huì)有兩件事發(fā)生

如果有 setImmediate 回調(diào)需要執(zhí)行,poll 階段會(huì)停止并且進(jìn)入到 check 階段執(zhí)行回調(diào)

如果沒有 setImmediate 回調(diào)需要執(zhí)行懈叹,會(huì)等待回調(diào)被加入到隊(duì)列中并立即執(zhí)行回調(diào)乖杠,這里同樣會(huì)有個(gè)超時(shí)時(shí)間設(shè)置防止一直等待下去

當(dāng)然設(shè)定了 timer 的話且 poll 隊(duì)列為空,則會(huì)判斷是否有 timer 超時(shí)澄成,如果有的話會(huì)回到 timer 階段執(zhí)行回調(diào)胧洒。

(3) check 階段

setImmediate()的回調(diào)會(huì)被加入 check 隊(duì)列中畏吓,從 event loop 的階段圖可以知道,check 階段的執(zhí)行順序在 poll 階段之后卫漫。

我們先來看個(gè)例子:

```console.log('start')setTimeout(() => {

? console.log('timer1')

? Promise.resolve().then(function(){

? ? console.log('promise1')

? })

}, 0)setTimeout(() => {

? console.log('timer2')

? Promise.resolve().then(function(){

? ? console.log('promise2')

? })

}, 0)Promise.resolve().then(function(){

? console.log('promise3')

})console.log('end')//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2

```

一開始執(zhí)行棧的同步任務(wù)(這屬于宏任務(wù))執(zhí)行完畢后(依次打印出 start end菲饼,并將 2 個(gè) timer 依次放入 timer 隊(duì)列),會(huì)先去執(zhí)行微任務(wù)(這點(diǎn)跟瀏覽器端的一樣),所以打印出 promise3

然后進(jìn)入 timers 階段列赎,執(zhí)行 timer1 的回調(diào)函數(shù)宏悦,打印 timer1,并將 promise.then 回調(diào)放入 microtask 隊(duì)列包吝,同樣的步驟執(zhí)行 timer2饼煞,打印 timer2;這點(diǎn)跟瀏覽器端相差比較大诗越,timers 階段有幾個(gè) setTimeout/setInterval 都會(huì)依次執(zhí)行砖瞧,并不像瀏覽器端,每執(zhí)行一個(gè)宏任務(wù)后就去執(zhí)行一個(gè)微任務(wù)(關(guān)于 Node 與瀏覽器的 Event Loop 差異嚷狞,下文還會(huì)詳細(xì)介紹)块促。

3. 注意點(diǎn)

(1) setTimeout 和 setImmediate

二者非常相似,區(qū)別主要在于調(diào)用時(shí)機(jī)不同床未。

setImmediate 設(shè)計(jì)在 poll 階段完成時(shí)執(zhí)行竭翠,即 check 階段;

setTimeout 設(shè)計(jì)在 poll 階段為空閑時(shí)即硼,且設(shè)定時(shí)間到達(dá)后執(zhí)行逃片,但它在 timer 階段執(zhí)行

```setTimeout(functiontimeout(){

? console.log('timeout');

},0);

setImmediate(functionimmediate(){

? console.log('immediate');

});

```

對(duì)于以上代碼來說,setTimeout 可能執(zhí)行在前只酥,也可能執(zhí)行在后褥实。

首先 setTimeout(fn, 0) === setTimeout(fn, 1),這是由源碼決定的

進(jìn)入事件循環(huán)也是需要成本的裂允,如果在準(zhǔn)備時(shí)候花費(fèi)了大于 1ms 的時(shí)間损离,那么在 timer 階段就會(huì)直接執(zhí)行 setTimeout 回調(diào)

如果準(zhǔn)備時(shí)間花費(fèi)小于 1ms,那么就是 setImmediate 回調(diào)先執(zhí)行了

但當(dāng)二者在異步 i/o callback 內(nèi)部調(diào)用時(shí)绝编,總是先執(zhí)行 setImmediate僻澎,再執(zhí)行 setTimeout

```constfs =require('fs')fs.readFile(__filename,() =>{setTimeout(() =>{console.log('timeout'); },0) setImmediate(() =>{console.log('immediate') })})// immediate// timeout```

在上述代碼中,setImmediate 永遠(yuǎn)先執(zhí)行十饥。因?yàn)閮蓚€(gè)代碼寫在 IO 回調(diào)中窟勃,IO 回調(diào)是在 poll 階段執(zhí)行,當(dāng)回調(diào)執(zhí)行完畢后隊(duì)列為空逗堵,發(fā)現(xiàn)存在 setImmediate 回調(diào)秉氧,所以就直接跳轉(zhuǎn)到 check 階段去執(zhí)行回調(diào)了。

(2) process.nextTick

這個(gè)函數(shù)其實(shí)是獨(dú)立于 Event Loop 之外的蜒秤,它有一個(gè)自己的隊(duì)列汁咏,當(dāng)每個(gè)階段完成后亚斋,如果存在 nextTick 隊(duì)列,就會(huì)清空隊(duì)列中的所有回調(diào)函數(shù)攘滩,并且優(yōu)先于其他 microtask 執(zhí)行帅刊。

```setTimeout(() => {

console.log('timer1')

Promise.resolve().then(function(){

? console.log('promise1')

})

}, 0)

process.nextTick(() => {

console.log('nextTick')

process.nextTick(() => {

? console.log('nextTick')

? process.nextTick(() => {

? ? console.log('nextTick')

? ? process.nextTick(() => {

? ? ? console.log('nextTick')

? ? })

? })

})

})

```

五、Node 與瀏覽器的 Event Loop 差異

瀏覽器環(huán)境下漂问,microtask 的任務(wù)隊(duì)列是每個(gè) macrotask 執(zhí)行完之后執(zhí)行赖瞒。而在 Node.js 中,microtask 會(huì)在事件循環(huán)的各個(gè)階段之間執(zhí)行级解,也就是一個(gè)階段執(zhí)行完畢冒黑,就會(huì)去執(zhí)行 microtask 隊(duì)列的任務(wù)


圖2

接下我們通過一個(gè)例子來說明兩者區(qū)別:

```setTimeout(()=>{

? ? console.log('timer1')

? ? Promise.resolve().then(function(){

? ? ? ? console.log('promise1')

? ? })

}, 0)setTimeout(()=>{

? ? console.log('timer2')

? ? Promise.resolve().then(function(){

? ? ? ? console.log('promise2')

? ? })

}, 0)

```

瀏覽器端運(yùn)行結(jié)果:timer1=>promise1=>timer2=>promise2

瀏覽器端的處理過程如下:


圖3

Node 端運(yùn)行結(jié)果:timer1=>timer2=>promise1=>promise2

全局腳本(main())執(zhí)行勤哗,將 2 個(gè) timer 依次放入 timer 隊(duì)列,main()執(zhí)行完畢掩驱,調(diào)用椕⒒空閑,任務(wù)隊(duì)列開始執(zhí)行欧穴;

首先進(jìn)入 timers 階段民逼,執(zhí)行 timer1 的回調(diào)函數(shù),打印 timer1涮帘,并將 promise1.then 回調(diào)放入 microtask 隊(duì)列拼苍,同樣的步驟執(zhí)行 timer2,打印 timer2调缨;

至此疮鲫,timer 階段執(zhí)行結(jié)束,event loop 進(jìn)入下一個(gè)階段之前弦叶,執(zhí)行 microtask 隊(duì)列的所有任務(wù)俊犯,依次打印 promise1、promise2

Node 端的處理過程如下:


圖4


總結(jié)

瀏覽器和 Node 環(huán)境下伤哺,microtask 任務(wù)隊(duì)列的執(zhí)行時(shí)機(jī)不同

Node 端燕侠,microtask 在事件循環(huán)的各個(gè)階段之間執(zhí)行

瀏覽器端,microtask 在事件循環(huán)的 macrotask 執(zhí)行完之后執(zhí)行

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末立莉,一起剝皮案震驚了整個(gè)濱河市绢彤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蜓耻,老刑警劉巖茫舶,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異媒熊,居然都是意外死亡奇适,警方通過查閱死者的電腦和手機(jī)坟比,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嚷往,“玉大人葛账,你說我怎么就攤上這事∑と剩” “怎么了籍琳?”我有些...
    開封第一講書人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長贷祈。 經(jīng)常有香客問我趋急,道長,這世上最難降的妖魔是什么势誊? 我笑而不...
    開封第一講書人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任呜达,我火速辦了婚禮,結(jié)果婚禮上粟耻,老公的妹妹穿的比我還像新娘查近。我一直安慰自己,他們只是感情好挤忙,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開白布霜威。 她就那樣靜靜地躺著,像睡著了一般册烈。 火紅的嫁衣襯著肌膚如雪戈泼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評(píng)論 1 291
  • 那天赏僧,我揣著相機(jī)與錄音大猛,去河邊找鬼。 笑死次哈,一個(gè)胖子當(dāng)著我的面吹牛胎署,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播窑滞,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼琼牧,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了哀卫?” 一聲冷哼從身側(cè)響起巨坊,我...
    開封第一講書人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎此改,沒想到半個(gè)月后趾撵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年占调,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了暂题。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡究珊,死狀恐怖薪者,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情剿涮,我是刑警寧澤言津,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站取试,受9級(jí)特大地震影響悬槽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瞬浓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一初婆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧猿棉,春花似錦烟逊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乔宿。三九已至位迂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間详瑞,已是汗流浹背掂林。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留坝橡,地道東北人泻帮。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像计寇,于是被迫代替她去往敵國和親锣杂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容