事件循環(huán)
事件循環(huán)被稱作循環(huán)的原因在于托呕,它一直在查找新的事件并且執(zhí)行拜英。一次循環(huán)的執(zhí)行稱之為 tick屋休, 在這個(gè)循環(huán)里執(zhí)行的代碼稱作 task
while (eventLoop.waitForTask()) {
eventLoop.processNextTask()
}
任務(wù)(Tasks)中同步執(zhí)行的代碼可能會(huì)在循環(huán)中生成新的任務(wù)霞怀。一個(gè)簡單的生成新任務(wù)的編程方式就是 setTimtout(taskFn, deley)
,當(dāng)然任務(wù)也可以從其他的資源產(chǎn)生欢嘿,比如用戶的事件、網(wǎng)絡(luò)事件或者DOM的繪制揩局。
任務(wù)隊(duì)列
讓事情變得復(fù)雜的情況是毫玖,事件循環(huán)可能有幾種任務(wù)任務(wù)隊(duì)列。唯一的兩個(gè)限制是同一個(gè)任務(wù)源中的事件必須屬于同一個(gè)隊(duì)列谐腰,并且必須在每個(gè)隊(duì)列中按插入順序處理任務(wù)孕豹。除了這些之外,執(zhí)行環(huán)境可以自由地做它所做的事情十气。例如,它可以決定下一步要處理哪些任務(wù)隊(duì)列春霍。
while (eventLoop.waitForTask()) {
const taskQueue = eventLoop.selectTaskQueue()
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask()
}
}
基于這個(gè)模型砸西,我們失去了對(duì)事件執(zhí)行時(shí)間的控制權(quán)。瀏覽器可能決定在執(zhí)行我們?cè)O(shè)定的setTimeout
之前先清空其他幾個(gè)隊(duì)列.
Microtask queue
幸運(yùn)的是址儒,事件循環(huán)也有一個(gè)單獨(dú)的隊(duì)列叫做 microtask芹枷,microtask 將會(huì)在百分百在當(dāng)前task隊(duì)列執(zhí)行完畢以后執(zhí)行
while (eventLoop.waitForTask()) {
const taskQueue = eventLoop.selectTaskQueue()
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask()
}
const microtaskQueue = eventLoop.microTaskQueue
while (microtaskQueue.hasNextMicrotask()) {
microtaskQueue.processNextMicrotask()
}
}
最簡單的方式生成一個(gè) microtask 任務(wù)是 Promise.resolve().then(microtaskFn)
, Microtasks 的插入執(zhí)行是按照順序的,而且因?yàn)橹挥幸粋€(gè)唯一的 microtask 隊(duì)列莲趣。執(zhí)行環(huán)境不會(huì)再搞錯(cuò)執(zhí)行的時(shí)間了鸳慈。
另外,microtask任務(wù) 也可以生成新的 microtask任務(wù) 并且插入到同樣的隊(duì)列中(插入當(dāng)前microtask)并且在同一個(gè) tick 里執(zhí)行
渲染
最后一個(gè)是關(guān)于渲染的任務(wù)喧伞,不同于其他的任務(wù)處理走芋,渲染任務(wù)并不是被獨(dú)立的后臺(tái)任務(wù)處理。它可能會(huì)是一個(gè)獨(dú)立運(yùn)行在每一個(gè)tick結(jié)束后的算法潘鲫。執(zhí)行環(huán)境擁有較大的選擇空間翁逞,它可能會(huì)在每一個(gè)任務(wù)隊(duì)列后執(zhí)行渲染,也可能執(zhí)行多個(gè)任務(wù)隊(duì)列而不渲染溉仑。
幸運(yùn)的是這里有一個(gè) requestAnimationFrame(handle)
函數(shù)挖函,它會(huì)正確的在下一次渲染時(shí)執(zhí)行內(nèi)置的函數(shù)
最后這就是我們整個(gè)的渲染模型
while (eventLoop.waitForTask()) {
const taskQueue = eventLoop.selectTaskQueue()
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask()
}
const microtaskQueue = eventLoop.microTaskQueue
while (microtaskQueue.hasNextMicrotask()) {
microtaskQueue.processNextMicrotask()
}
if (shouldRender()) {
applyScrollResizeAndCSS()
runAnimationFrames()
render()
}
}
以上內(nèi)容翻譯自writing-a-javascript-framework-execution-timing-beyond-settimeout
思考
以上就是對(duì)整個(gè)event loop的翻譯與解釋,文章解釋比較簡潔明細(xì)浊竟,但是相信大部分同學(xué)可能還是不太明白怨喘,那么我們換個(gè)思路津畸,如果面試官問什么是event loop,面試官是想知道些什么必怜?我應(yīng)該怎么回答肉拓?
event loop顧名思義就是事件循環(huán),為什么要有事件循環(huán)呢棚赔?因?yàn)閂8是單線程的帝簇,即同一時(shí)間只能干一件事情,但是呢文件的讀取靠益,網(wǎng)絡(luò)的IO處理是很緩慢的丧肴,并且是不確定的,如果同步等待它們響應(yīng),那么用戶就起飛了胧后。于是我們就把這個(gè)事件加入到一個(gè) 事件隊(duì)列里(task),等到事件完成時(shí)芋浮,event loop再執(zhí)行一個(gè)事件隊(duì)列。
值得注意的是壳快,每一種異步事件加入的 事件隊(duì)列是不一樣的纸巷。唯一的兩個(gè)限制是同一個(gè)任務(wù)源中的事件必須屬于同一個(gè)隊(duì)列,并且必須在每個(gè)隊(duì)列中按插入順序處理任務(wù)眶痰。 也就是說由系統(tǒng)提供的執(zhí)行task的方法瘤旨,如 setTimeout setInterval setimmediate 會(huì)在一個(gè)task,網(wǎng)絡(luò)IO會(huì)在一個(gè)task竖伯,用戶的事件會(huì)在一個(gè)task存哲。event-loop將會(huì)按照以下順序執(zhí)行
update_time
在事件循環(huán)的開頭,這一步的作用實(shí)際上是為了獲取一下系統(tǒng)時(shí)間七婴,以保證之后的timer有個(gè)計(jì)時(shí)的標(biāo)準(zhǔn)祟偷。這個(gè)動(dòng)作會(huì)在每次事件循環(huán)的時(shí)候都發(fā)生,確保了之后timer觸發(fā)的準(zhǔn)確性打厘。(其實(shí)也不太準(zhǔn)確....)timers
事件循環(huán)跑到這個(gè)階段的時(shí)候修肠,要檢查是否有到期的timer,其實(shí)也就是setTimeout和setInterval這種類型的timer,到期了户盯,就會(huì)執(zhí)行他們的回調(diào)嵌施。I/O callbacks
處理異步事件的回調(diào),比如網(wǎng)絡(luò)I/O先舷,比如文件讀取I/O艰管。當(dāng)這些I/O動(dòng)作都結(jié)束的時(shí)候,在這個(gè)階段會(huì)觸發(fā)它們的回調(diào)蒋川。idle, prepare
這個(gè)階段內(nèi)部做一些動(dòng)作牲芋,與理解事件循環(huán)沒啥關(guān)系I/O poll階段
這個(gè)階段相當(dāng)有意思,也是事件循環(huán)設(shè)計(jì)的一個(gè)有趣的點(diǎn)。這個(gè)階段是選擇運(yùn)行的缸浦。選擇運(yùn)行的意思就是不一定會(huì)運(yùn)行夕冲。check
執(zhí)行setImmediate操作close callbacks
關(guān)閉I/O的動(dòng)作,比如文件描述符的關(guān)閉裂逐,鏈接斷開歹鱼,等等等
(以上參考自方正——Node.js源碼解析:深入Libuv理解事件循環(huán))
除了task還有一個(gè)microtask,這一個(gè)概念是ES6提出Promise以后出現(xiàn)的卜高。這個(gè)microtask queue只有一個(gè)弥姻。并且會(huì)在且一定會(huì)在每一個(gè)task后執(zhí)行,且執(zhí)行是按順序的掺涛。加入到microtask 的事件類型有Promise.resolve().then(), process.nextTick() 值得注意的是庭敦,event loop一定會(huì)在執(zhí)行完micrtask以后才會(huì)尋找新的 可執(zhí)行的task隊(duì)列湿故。而microtask事件內(nèi)部又可以產(chǎn)生新的microtask事件比如
(function microtask() {
process.nextTick(() => microtask())
})()
這樣就會(huì)不斷的在microtask queue添加事件匹耕,導(dǎo)致整個(gè)eventloop堵塞
最后就是一個(gè)渲染的事件隊(duì)列,這個(gè)隊(duì)列只出現(xiàn)在瀏覽器上汁果,并且執(zhí)行環(huán)境會(huì)根據(jù)情況決定執(zhí)行與否(可能執(zhí)行很多task queue也不執(zhí)行渲染隊(duì)列)拣帽。它如果執(zhí)行則一定會(huì)在microtask后執(zhí)行疼电,通過requestAnimationFrame(handle)
方法,能夠保證中間的代碼一定能在下一次執(zhí)行渲染函數(shù)前執(zhí)行
補(bǔ)充常見的產(chǎn)生microtask和task事件的方法
microtasks:
- process.nextTick
- promise
- Object.observe
- MutationObserver
tasks:
- setTimeout
- setInterval
- setImmediate
- I/O
- UI渲染
Tips
- 我們通過node運(yùn)行一個(gè)js文件,如果沒有可執(zhí)行事件的事件隊(duì)列减拭,進(jìn)程就會(huì)退出蔽豺,那么怎么不讓它退出呢?
setInterval方法拧粪,這貨會(huì)一直循環(huán)建立新的事件茫虽,這樣能夠保證node進(jìn)程不退出
監(jiān)聽 beforeExit 事件,通過process.on('beforeExit', handle) 這個(gè)事件在node進(jìn)程退出前會(huì)觸發(fā)既们,但是如果這里面的handle包含了一個(gè)可以生成異步事件的操作,則node進(jìn)程也不會(huì)退出正什。手動(dòng)觸發(fā)process.exit(EXIT_CODE)不會(huì)觸發(fā)該事件
- setInterval會(huì)導(dǎo)致node進(jìn)程不能正常退出啥纸,但是如果希望即使有setInterval也能正常退出怎么辦(有一些循環(huán)并不希望掛起node進(jìn)程)?
const timer = process.setInterval(handle, deley) 調(diào)用setInterval方法會(huì)返回一個(gè)timer婴氮,調(diào)用 timer.unref() 則event-loop判斷除它以外斯棒,沒有可進(jìn)行的事件隊(duì)列后也會(huì)推出
- process.on('exit', handle)中,handle里的異步事件不能執(zhí)行
exit事件在手動(dòng)執(zhí)行process.exit(EXIT_CODE)后主经,或者event loop中沒有可執(zhí)行的事件隊(duì)列 時(shí)觸發(fā)荣暮。觸發(fā) exit 事件后,執(zhí)行環(huán)境就不會(huì)再生成新的 事件隊(duì)列了罩驻,因此這里面的異步事件都會(huì)被強(qiáng)制隊(duì)列
最后
以上都是我瞎編的
如果你喜歡我瞎編的文章穗酥,歡迎star Github