事件循環
事件循環被稱作循環的原因在于,它一直在查找新的事件并且執行。一次循環的執行稱之為 tick, 在這個循環里執行的代碼稱作 task
while (eventLoop.waitForTask()) {
eventLoop.processNextTask()
}
任務(Tasks)中同步執行的代碼可能會在循環中生成新的任務。一個簡單的生成新任務的編程方式就是 setTimtout(taskFn, deley)
,當然任務也可以從其他的資源產生,比如用戶的事件、網絡事件或者DOM的繪制。
任務隊列
讓事情變得復雜的情況是,事件循環可能有幾種任務任務隊列。唯一的兩個限制是同一個任務源中的事件必須屬于同一個隊列,并且必須在每個隊列中按插入順序處理任務。除了這些之外,執行環境可以自由地做它所做的事情。例如,它可以決定下一步要處理哪些任務隊列。
while (eventLoop.waitForTask()) {
const taskQueue = eventLoop.selectTaskQueue()
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask()
}
}
基于這個模型,我們失去了對事件執行時間的控制權。瀏覽器可能決定在執行我們設定的setTimeout
之前先清空其他幾個隊列.
Microtask queue
幸運的是,事件循環也有一個單獨的隊列叫做 microtask,microtask 將會在百分百在當前task隊列執行完畢以后執行
while (eventLoop.waitForTask()) {
const taskQueue = eventLoop.selectTaskQueue()
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask()
}
const microtaskQueue = eventLoop.microTaskQueue
while (microtaskQueue.hasNextMicrotask()) {
microtaskQueue.processNextMicrotask()
}
}
最簡單的方式生成一個 microtask 任務是 Promise.resolve().then(microtaskFn)
, Microtasks 的插入執行是按照順序的,而且因為只有一個唯一的 microtask 隊列。執行環境不會再搞錯執行的時間了。
另外,microtask任務 也可以生成新的 microtask任務 并且插入到同樣的隊列中(插入當前microtask)并且在同一個 tick 里執行
渲染
最后一個是關于渲染的任務,不同于其他的任務處理,渲染任務并不是被獨立的后臺任務處理。它可能會是一個獨立運行在每一個tick結束后的算法。執行環境擁有較大的選擇空間,它可能會在每一個任務隊列后執行渲染,也可能執行多個任務隊列而不渲染。
幸運的是這里有一個 requestAnimationFrame(handle)
函數,它會正確的在下一次渲染時執行內置的函數
最后這就是我們整個的渲染模型
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()
}
}
以上內容翻譯自writing-a-javascript-framework-execution-timing-beyond-settimeout
思考
以上就是對整個event loop的翻譯與解釋,文章解釋比較簡潔明細,但是相信大部分同學可能還是不太明白,那么我們換個思路,如果面試官問什么是event loop,面試官是想知道些什么?我應該怎么回答?
event loop顧名思義就是事件循環,為什么要有事件循環呢?因為V8是單線程的,即同一時間只能干一件事情,但是呢文件的讀取,網絡的IO處理是很緩慢的,并且是不確定的,如果同步等待它們響應,那么用戶就起飛了。于是我們就把這個事件加入到一個 事件隊列里(task),等到事件完成時,event loop再執行一個事件隊列。
值得注意的是,每一種異步事件加入的 事件隊列是不一樣的。唯一的兩個限制是同一個任務源中的事件必須屬于同一個隊列,并且必須在每個隊列中按插入順序處理任務。 也就是說由系統提供的執行task的方法,如 setTimeout setInterval setimmediate 會在一個task,網絡IO會在一個task,用戶的事件會在一個task。event-loop將會按照以下順序執行
update_time
在事件循環的開頭,這一步的作用實際上是為了獲取一下系統時間,以保證之后的timer有個計時的標準。這個動作會在每次事件循環的時候都發生,確保了之后timer觸發的準確性。(其實也不太準確....)timers
事件循環跑到這個階段的時候,要檢查是否有到期的timer,其實也就是setTimeout和setInterval這種類型的timer,到期了,就會執行他們的回調。I/O callbacks
處理異步事件的回調,比如網絡I/O,比如文件讀取I/O。當這些I/O動作都結束的時候,在這個階段會觸發它們的回調。idle, prepare
這個階段內部做一些動作,與理解事件循環沒啥關系I/O poll階段
這個階段相當有意思,也是事件循環設計的一個有趣的點。這個階段是選擇運行的。選擇運行的意思就是不一定會運行。check
執行setImmediate操作close callbacks
關閉I/O的動作,比如文件描述符的關閉,鏈接斷開,等等等
(以上參考自方正——Node.js源碼解析:深入Libuv理解事件循環)
除了task還有一個microtask,這一個概念是ES6提出Promise以后出現的。這個microtask queue只有一個。并且會在且一定會在每一個task后執行,且執行是按順序的。加入到microtask 的事件類型有Promise.resolve().then(), process.nextTick() 值得注意的是,event loop一定會在執行完micrtask以后才會尋找新的 可執行的task隊列。而microtask事件內部又可以產生新的microtask事件比如
(function microtask() {
process.nextTick(() => microtask())
})()
這樣就會不斷的在microtask queue添加事件,導致整個eventloop堵塞
最后就是一個渲染的事件隊列,這個隊列只出現在瀏覽器上,并且執行環境會根據情況決定執行與否(可能執行很多task queue也不執行渲染隊列)。它如果執行則一定會在microtask后執行,通過requestAnimationFrame(handle)
方法,能夠保證中間的代碼一定能在下一次執行渲染函數前執行
補充常見的產生microtask和task事件的方法
microtasks:
- process.nextTick
- promise
- Object.observe
- MutationObserver
tasks:
- setTimeout
- setInterval
- setImmediate
- I/O
- UI渲染
Tips
- 我們通過node運行一個js文件,如果沒有可執行事件的事件隊列,進程就會退出,那么怎么不讓它退出呢?
setInterval方法,這貨會一直循環建立新的事件,這樣能夠保證node進程不退出
監聽 beforeExit 事件,通過process.on('beforeExit', handle) 這個事件在node進程退出前會觸發,但是如果這里面的handle包含了一個可以生成異步事件的操作,則node進程也不會退出。手動觸發process.exit(EXIT_CODE)不會觸發該事件
- setInterval會導致node進程不能正常退出,但是如果希望即使有setInterval也能正常退出怎么辦(有一些循環并不希望掛起node進程)?
const timer = process.setInterval(handle, deley) 調用setInterval方法會返回一個timer,調用 timer.unref() 則event-loop判斷除它以外,沒有可進行的事件隊列后也會推出
- process.on('exit', handle)中,handle里的異步事件不能執行
exit事件在手動執行process.exit(EXIT_CODE)后,或者event loop中沒有可執行的事件隊列 時觸發。觸發 exit 事件后,執行環境就不會再生成新的 事件隊列了,因此這里面的異步事件都會被強制隊列
最后
以上都是我瞎編的
如果你喜歡我瞎編的文章,歡迎star Github