概述:
event loop(事件循環(huán))是一個(gè)執(zhí)行模型,在不同的地方有不同的實(shí)現(xiàn)。瀏覽器和NodeJS基于不同的技術(shù)實(shí)現(xiàn)了各自的Event Loop。
宏隊(duì)列:
宏隊(duì)列,macrotask,也叫tasks。一些異步任務(wù)的回調(diào)會(huì)依次進(jìn)入macrotask queue,等待后續(xù)被調(diào)用,這些異步任務(wù)包括:
setTimeout
setInterval
setImmediate (Node獨(dú)有)
requestAnimationFrame (瀏覽器獨(dú)有)
I/O
UI rendering (瀏覽器獨(dú)有)
微隊(duì)列:
微隊(duì)列,microtask,也叫jobs。另一些異步任務(wù)的回調(diào)會(huì)依次進(jìn)入micro task queue,等待后續(xù)被調(diào)用,這些異步任務(wù)包括:
process.nextTick (Node獨(dú)有)
Promise
Object.observe
MutationObserver
(注:這里只針對(duì)瀏覽器和NodeJS)
瀏覽器的Event Loop:
1.在主線程執(zhí)行同步代碼,完了以后
2.把微隊(duì)列的排名第一的任務(wù)揪出來(lái)放到主線程執(zhí)行,完了以后把微隊(duì)列的排名第一的任務(wù)揪出來(lái)放到主線程執(zhí)行....一直干空
3.把宏隊(duì)列的排名第一的任務(wù)揪出來(lái)放到主線程執(zhí)行,完了以后把微隊(duì)列的排名第一的任務(wù)揪出來(lái)放到主線程執(zhí)行,完了以后把微隊(duì)列的排名第一的任務(wù)揪出來(lái)放到主線程執(zhí)行....一直干空
323232323232........
PS:注意,如果在執(zhí)行microtask的過(guò)程中,又產(chǎn)生了microtask,那么會(huì)加入到隊(duì)列的末尾,也會(huì)在這個(gè)周期被調(diào)用執(zhí)行;
NodeJS中的Event Loop:
但是在瀏覽器中,可以認(rèn)為只有一個(gè)宏隊(duì)列,所有的macrotask都會(huì)被加到這一個(gè)宏隊(duì)列中,但是在NodeJS中,不同的macrotask會(huì)被放置在不同的宏隊(duì)列中。NodeJS的Event Loop中,執(zhí)行宏隊(duì)列的回調(diào)任務(wù)有6個(gè)階段:
各個(gè)階段執(zhí)行的任務(wù)如下:
timers階段:這個(gè)階段執(zhí)行setTimeout和setInterval預(yù)定的callback
I/O callback階段:執(zhí)行除了close事件的callbacks、被timers設(shè)定的callbacks、setImmediate()設(shè)定的callbacks這些之外的callbacks
idle, prepare階段:僅node內(nèi)部使用
poll階段:獲取新的I/O事件,適當(dāng)?shù)臈l件下node將阻塞在這里
check階段:執(zhí)行setImmediate()設(shè)定的callbacks
close callbacks階段:執(zhí)行socket.on('close', ....)這些callbacks
每個(gè)階段都有一個(gè)「先入先出隊(duì)列」,這個(gè)隊(duì)列存有要執(zhí)行的回調(diào)函數(shù)(譯注:存的是函數(shù)地址)。不過(guò)每個(gè)階段都有其特有的使命。一般來(lái)說(shuō),當(dāng) event loop 達(dá)到某個(gè)階段時(shí),會(huì)在這個(gè)階段進(jìn)行一些特殊的操作,然后執(zhí)行這個(gè)階段的隊(duì)列里的所有回調(diào)。
什么時(shí)候停止執(zhí)行這些回調(diào)呢?下列兩種情況之一會(huì)停止:
1.隊(duì)列的操作全被執(zhí)行完了
2.執(zhí)行的回調(diào)數(shù)目到達(dá)指定的最大值
然后,event loop 進(jìn)入下一個(gè)階段,然后再下一個(gè)階段。
一方面,上面這些操作都有可能添加計(jì)時(shí)器;另一方面,操作系統(tǒng)會(huì)向 poll 隊(duì)列中添加新的事件,當(dāng) poll 隊(duì)列中的事件被處理時(shí)可能會(huì)有新的 poll 事件進(jìn)入 poll 隊(duì)列。結(jié)果,耗時(shí)較長(zhǎng)的回調(diào)函數(shù)可以讓 event loop 在 poll 階段停留很久,久到錯(cuò)過(guò)了計(jì)時(shí)器的觸發(fā)時(shí)機(jī)。你可以在下文的 timers 章節(jié)和 poll 章節(jié)詳細(xì)了解這其中的細(xì)節(jié)。
timers 階段
計(jì)時(shí)器實(shí)際上是在指定多久以后可以執(zhí)行某個(gè)回調(diào)函數(shù),而不是指定某個(gè)函數(shù)的確切執(zhí)行時(shí)間。當(dāng)指定的時(shí)間達(dá)到后,計(jì)時(shí)器的回調(diào)函數(shù)會(huì)盡早被執(zhí)行。如果操作系統(tǒng)很忙,或者 Node.js 正在執(zhí)行一個(gè)耗時(shí)的函數(shù),那么計(jì)時(shí)器的回調(diào)函數(shù)就會(huì)被推遲執(zhí)行。
注意,從原理上來(lái)說(shuō),poll 階段能控制計(jì)時(shí)器的回調(diào)函數(shù)什么時(shí)候被執(zhí)行。
舉例來(lái)說(shuō),你設(shè)置了一個(gè)計(jì)時(shí)器在 100 毫秒后執(zhí)行,然后你的腳本用了 95 毫秒來(lái)異步讀取了一個(gè)文件當(dāng) event loop 進(jìn)入 poll 階段,發(fā)現(xiàn) poll 隊(duì)列為空(因?yàn)槲募€沒(méi)讀完),event loop 檢查了一下最近的計(jì)時(shí)器,大概還有 100 毫秒時(shí)間,于是 event loop 決定這段時(shí)間就停在 poll 階段。在 poll 階段停了 95 毫秒之后,fs.readFile 操作完成,一個(gè)耗時(shí) 10 毫秒的回調(diào)函數(shù)被系統(tǒng)放入 poll 隊(duì)列,于是 event loop 執(zhí)行了這個(gè)回調(diào)函數(shù)。執(zhí)行完畢后,poll 隊(duì)列為空,于是 event loop 去看了一眼最近的計(jì)時(shí)器(譯注:event loop 發(fā)現(xiàn)臥槽,已經(jīng)超時(shí) 95 + 10 - 100 = 5 毫秒了),于是經(jīng)由 check 階段、close callbacks 階段繞回到 timers 階段,執(zhí)行 timers 隊(duì)列里的那個(gè)回調(diào)函數(shù)。這個(gè)例子中,100 毫秒的計(jì)時(shí)器實(shí)際上是在 105 毫秒后才執(zhí)行的。
PS:注意:為了防止 poll 階段占用了 event loop 的所有時(shí)間,libuv(Node.js 用來(lái)實(shí)現(xiàn) event loop 和所有異步行為的 C 語(yǔ)言寫(xiě)成的庫(kù))對(duì) poll 階段的最長(zhǎng)停留時(shí)間做出了限制,具體時(shí)間因操作系統(tǒng)而異。
I/O callbacks 階段
這個(gè)階段會(huì)執(zhí)行一些系統(tǒng)操作的回調(diào)函數(shù),比如 TCP 報(bào)錯(cuò),如果一個(gè) TCP socket 開(kāi)始連接時(shí)出現(xiàn)了 ECONNREFUSED 錯(cuò)誤,一些 *nix 系統(tǒng)就會(huì)(向 Node.js)通知這個(gè)錯(cuò)誤。這個(gè)通知就會(huì)被放入 I/O callbacks 隊(duì)列。
poll 階段(輪詢階段)
poll 階段有兩個(gè)功能:
1.如果發(fā)現(xiàn)計(jì)時(shí)器的時(shí)間到了,就繞回到 timers 階段執(zhí)行計(jì)時(shí)器的回調(diào)。
2.然后再,執(zhí)行 poll 隊(duì)列里的回調(diào)。
當(dāng) event loop 進(jìn)入 poll 階段,如果發(fā)現(xiàn)沒(méi)有計(jì)時(shí)器,就會(huì):
1.如果 poll 隊(duì)列不是空的,event loop 就會(huì)依次執(zhí)行隊(duì)列里的回調(diào)函數(shù),直到隊(duì)列被清空或者到達(dá) poll 階段的時(shí)間上限。
2.如果 poll 隊(duì)列是空的,就會(huì):
????????1.如果有 setImmediate() 任務(wù),event loop 就結(jié)束 poll 階段去往 check 階段。
????????2.如果沒(méi)有 setImmediate() 任務(wù),event loop 就會(huì)等待新的回調(diào)函數(shù)進(jìn)入 poll 隊(duì)列,并立即執(zhí)行它。
一旦 poll 隊(duì)列為空,event loop 就會(huì)檢查計(jì)時(shí)器有沒(méi)有到期,如果有計(jì)時(shí)器到期了,event loop 就會(huì)回到 timers 階段執(zhí)行計(jì)時(shí)器的回調(diào)。
check 階段
這個(gè)階段允許開(kāi)發(fā)者在 poll 階段結(jié)束后立即執(zhí)行一些函數(shù)。如果 poll 階段空閑了,同時(shí)存在 setImmediate() 任務(wù),event loop 就會(huì)進(jìn)入 check 階段。
setImmediate() 實(shí)際上是一種特殊的計(jì)時(shí)器,有自己特有的階段。它是通過(guò) libuv 里一個(gè)能將回調(diào)安排在 poll 階段之后執(zhí)行的 API 實(shí)現(xiàn)的。
一般來(lái)說(shuō),當(dāng)代碼執(zhí)行后,event loop 最終會(huì)達(dá)到 poll 階段,等待新的連接、新的請(qǐng)求等。但是如果一個(gè)回調(diào)是由 setImmediate() 發(fā)出的,同時(shí) poll 階段空閑下來(lái)了,event loop就會(huì)結(jié)束 poll 階段進(jìn)入 check 階段,不再等待新的 poll 事件。
close callbacks 階段
如果一個(gè) socket 或者 handle 被突然關(guān)閉(比如 socket.destroy()),那么就會(huì)有一個(gè) close 事件進(jìn)入這個(gè)階段
NodeJS中微隊(duì)列主要有2個(gè):
1.Next Tick Queue:是放置process.nextTick(callback)的回調(diào)任務(wù)的
2.Other Micro Queue:放置其他microtask,比如Promise等
在瀏覽器中,也可以認(rèn)為只有一個(gè)微隊(duì)列,所有的microtask都會(huì)被加到這一個(gè)微隊(duì)列中,但是在NodeJS中,不同的microtask會(huì)被放置在不同的微隊(duì)列中。
NodeJS的Event Loop過(guò)程:
1.初始化EventLoop
2.執(zhí)行腳本同步代碼
2.執(zhí)行microtask微任務(wù),先執(zhí)行所有Next Tick Queue中的所有任務(wù),再執(zhí)行Other Microtask Queue中的所有任務(wù)
3.開(kāi)始執(zhí)行macrotask宏任務(wù),共6個(gè)階段,先從第1個(gè)階段開(kāi)始執(zhí)行相應(yīng)每一個(gè)階段macrotask中的所有任務(wù),
4.回到第二步,然后進(jìn)入宏隊(duì)列的第二個(gè)階段,去執(zhí)行完所有任務(wù),完了再回到第二步,然后第三階段.......
setImmediate 和 setTimeout :
setImmediate 和 setTimeout 很相似,但是其回調(diào)函數(shù)的調(diào)用時(shí)機(jī)卻不一樣。
setImmediate() 的作用是在當(dāng)前 poll 階段結(jié)束后調(diào)用一個(gè)函數(shù)。
setTimeout() 的作用是在一段時(shí)間后調(diào)用一個(gè)函數(shù)。
這兩者的回調(diào)的執(zhí)行順序取決于 setTimeout 和 setImmediate 被調(diào)用時(shí)的環(huán)境。
如果 setTimeout 和 setImmediate 都是在主模塊(main module)中被調(diào)用的,那么回調(diào)的執(zhí)行順序取決于當(dāng)前進(jìn)程的性能,這個(gè)性能受其他應(yīng)用程序進(jìn)程的影響。
舉個(gè)例子,HTML5規(guī)范規(guī)定,setTimeout最少4毫秒延時(shí),如果剛開(kāi)始加載腳本用了5毫秒,當(dāng)時(shí)間循環(huán)開(kāi)始,執(zhí)行宏任務(wù)的時(shí)候,tirmer階段發(fā)現(xiàn)setTimeout已經(jīng)超時(shí)間了,那么立即執(zhí)行它。然而check階段是排在tirmmer后面的,所以setTimeout先執(zhí)行,
如果3毫秒就加載完了腳本,進(jìn)入tirmer后發(fā)現(xiàn)setTimeout沒(méi)到時(shí)間,就往下走,停留在poll階段發(fā)現(xiàn)setTimeout到時(shí)間了,趕緊回到tirmer去執(zhí)行,但是check階段是必經(jīng)之路,于是setlmmediate是要先執(zhí)行的。
但是,如果把上面代碼放到 I/O 操作的回調(diào)里,setImmediate 的回調(diào)就總是優(yōu)先于 setTimeout 的回調(diào)
setImmediate 和 setTimeout?setImmediate 和 setTimeout?setImmediate 和 setTimeout?setImmediate 和 setTimeout?setImmediate 和 setTimeout?