瀏覽器中的 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)入下一階段。
從上圖中盒使,大致看出 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ù)。
接下我們通過一個(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
瀏覽器端的處理過程如下:
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 端的處理過程如下:
總結(jié)
瀏覽器和 Node 環(huán)境下伤哺,microtask 任務(wù)隊(duì)列的執(zhí)行時(shí)機(jī)不同
Node 端燕侠,microtask 在事件循環(huán)的各個(gè)階段之間執(zhí)行
瀏覽器端,microtask 在事件循環(huán)的 macrotask 執(zhí)行完之后執(zhí)行