JS異步編程(2)-異步核心Event loop

Event loop 是 JavaScript 異步編程的核心,通過(guò)事件循環(huán)機(jī)制,讓單線程的 JavaScript 具備異步處理任務(wù)的能力

異步任務(wù)隊(duì)列

異步任務(wù)隊(duì)列分為兩類

  • 宏任務(wù)隊(duì)列
  • 微任務(wù)隊(duì)列
    都用于存放異步任務(wù)

為什么異步隊(duì)列要分宏微任務(wù)?

其實(shí)在學(xué)習(xí)了 Event loop 很久之后,才突然反應(yīng)過(guò)來(lái),反問(wèn)自己這個(gè)最初的問(wèn)題
異步隊(duì)列有一個(gè)就行了,已經(jīng)能夠滿足異步操作的需求,為什么還需要分兩種隊(duì)列呢?

答案是:為了插隊(duì)!

宏微任務(wù)的執(zhí)行邏輯,本質(zhì)上就是為了滿足異步操作的插隊(duì)需求,讓某個(gè)后插入的異步操作盡量早的執(zhí)行

宏任務(wù)(macrotasks)

API Web Node
DOM API ? ?
I/O ? ?
setTimeout ? ?
setInterval ? ?
setImmediate ? ?
requestAnimationFrame ? ?

有些地方會(huì)把 UI Rendering 也列為宏任務(wù)
但是在 HTML 規(guī)范文檔中,發(fā)現(xiàn)這其實(shí)是和微任務(wù)平行的一個(gè)操作步驟

  • UI Rendering 代表的不是一個(gè)單一的任務(wù),而是一個(gè)任務(wù)隊(duì)列(queue)
  • 具備一些不同于普通宏任務(wù)和微任務(wù)的特性
    • 觸發(fā)的時(shí)機(jī)在當(dāng)前微任務(wù)隊(duì)列和下一個(gè)宏任務(wù)之間
    • UI Rendering 執(zhí)行中觸發(fā)的新的 requestAnimationFrame,不會(huì)推進(jìn)當(dāng)前正在執(zhí)行的 UI Rendering 隊(duì)列。而是進(jìn)入下一次的 UI Rendering 隊(duì)列

微任務(wù)(microtasks)

API Web Node
process.nextTick ? ?
MutationObserver ? ?
Promise.then catch finally ? ?

process.nextTick 和 web 端的 UI Rendering 類似

  • process.nextTick 也有一個(gè)自己的任務(wù)隊(duì)列 nextTick queue
  • 具備一些不同于普通微任務(wù)的特性
    • 觸發(fā)的時(shí)機(jī)在當(dāng)前宏任務(wù)和當(dāng)前微任務(wù)隊(duì)列之間

執(zhí)行機(jī)制

Web 中的執(zhí)行機(jī)制

瀏覽器環(huán)境下的 Event loop 是由HTML5規(guī)范明確定義,由各大瀏覽器廠商各自實(shí)現(xiàn)
這里主要涉及到下面幾個(gè)瀏覽器線程:

  • JS引擎線程:主要處理主執(zhí)行棧任務(wù)(同步任務(wù))
  • 異步http請(qǐng)求線程:主要處理網(wǎng)絡(luò)請(qǐng)求,將已完成的網(wǎng)絡(luò)請(qǐng)求回調(diào)函數(shù)推進(jìn)事件觸發(fā)線程
  • 定時(shí)器線程:將已完成待執(zhí)行的定時(shí)器回調(diào)函數(shù)推進(jìn)事件觸發(fā)線程
  • 事件觸發(fā)線程:存儲(chǔ)宏微任務(wù)的線程

基本流程

異步隊(duì)列的執(zhí)行機(jī)制,簡(jiǎn)單來(lái)說(shuō)

  1. 當(dāng)主執(zhí)行棧里的任務(wù)清空之后,開(kāi)始讀取異步任務(wù)隊(duì)列中的任務(wù)
  2. 先讀取微任務(wù)隊(duì)列中的任務(wù),依次讀取執(zhí)行直至隊(duì)列清空
  3. 然后從宏任務(wù)中讀取第一個(gè)任務(wù)執(zhí)行
  4. 從第2步開(kāi)始重復(fù),直到宏任務(wù)隊(duì)列為空

同步任務(wù) -> 全部微任務(wù) -> UI Rendering -> 宏任務(wù) -> 全部微任務(wù) -> UI Rendering -> 下一個(gè)宏任務(wù) -> ...

如果在執(zhí)行過(guò)程中

  • 觸發(fā)新的宏任務(wù),會(huì)將其推進(jìn)宏任務(wù)隊(duì)列,等待讀取

  • 觸發(fā)新的微任務(wù),會(huì)將其推進(jìn)當(dāng)前的微任務(wù)隊(duì)列,在本次微任務(wù)隊(duì)列中完成執(zhí)行
    同步任務(wù) -> 全部微任務(wù) -> UI Rendering -> 宏任務(wù)(觸發(fā)新的宏任務(wù)和微任務(wù)) -> 全部微任務(wù)(包含新觸發(fā)的微任務(wù)) -> UI Rendering -> 下一個(gè)宏任務(wù)(新觸發(fā)的宏任務(wù)被推進(jìn)宏任務(wù)列表等待執(zhí)行) -> ...

  • 觸發(fā)新的 UI Rendering,會(huì)將其推進(jìn)下一個(gè) UI Rendering
    同步任務(wù) -> 全部微任務(wù) -> UI Rendering(觸發(fā)新的 RAF) -> 宏任務(wù) -> 全部微任務(wù) -> UI Rendering(包含之前觸發(fā)的新RAF) -> 下一個(gè)宏任務(wù) -> ...

操作觸發(fā)的瀏覽器事件回調(diào)

// html
<div class="parent" onclick="handleClick()">
    <div class="child" onclick="handleClick()"/>
</div>

// js
function handleClick() {
    Promise.resolve().then(() => console.log('promise then'))
    setTimeout(() => console.log('setTimeout msg'), 0)
}

上面的代碼,如果用戶點(diǎn)擊 child 元素
類似于用宏任務(wù)的觸發(fā)方式,直接注冊(cè)了 parentchild 元素的 click 回調(diào)函數(shù)
child click -> child promise then -> parent click -> parent promise then -> child setTimeout msg -> parent setTimeout msg

代碼觸發(fā)的瀏覽器事件回調(diào)

同樣是上面的代碼,如果使用 JS 代碼觸發(fā)事件

document.querySelector('.child').click()

那么和 dispatchEvent 類似,都是一種同步任務(wù)的觸發(fā)方式
把兩次的 click 事件都推入主執(zhí)行棧隊(duì)列
child click -> parent click -> child promise then -> parent promise then -> child setTimeout msg -> parent setTimeout msg

Node 中的執(zhí)行機(jī)制

與 Web 端 Event loop 依賴瀏覽器線程一樣,Node 端 Event loop 也依賴一位新同學(xué): libuv

  • libuv 是 Node 的新跨平臺(tái)抽象層,核心是提供 i/o 的事件循環(huán)和異步回調(diào)
  • libuv使用異步,事件驅(qū)動(dòng)的編程方式
  • libuv的API包含有時(shí)間,非阻塞的網(wǎng)絡(luò),異步文件操作,子進(jìn)程等等。
  • Event Loop就是在libuv中實(shí)現(xiàn)的。

6個(gè)階段

Node的 Event loop一共分為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)入下一階段

image.png

每個(gè)細(xì)節(jié)具體如下:

  1. timers: 執(zhí)行 setTimeout 和 setInterval 中到期的 callback,由 poll 調(diào)度進(jìn)入該階段
  2. pending: 某些系統(tǒng)操作級(jí)別的回調(diào),在這個(gè)階段執(zhí)行
  3. idle, prepare: 僅在內(nèi)部使用。
  4. poll: 執(zhí)行 I/O 回調(diào),在適當(dāng)?shù)那闆r下回阻塞在這個(gè)階段。
  5. check: 執(zhí)行 setImmediate 的回調(diào)函數(shù)
  6. close: 執(zhí)行close事件的 callback
timers
  • timers 階段會(huì)執(zhí)行 setTimeout 和 setInterval 回調(diào),由 poll 調(diào)度進(jìn)入該階段
  • timers 階段如果觸發(fā)了新的 setTimeout 和 setInterval,會(huì)推入到下一次的 timers 階段,不會(huì)在本次 timers 階段執(zhí)行
poll

這一階段主要處理兩件事情

  1. 回到 timers 階段執(zhí)行回調(diào)
  2. 執(zhí)行 I/O 回調(diào)

執(zhí)行邏輯:


image.png

Node 10.x 及以前的基本流程

在 Node 10.x 及以前。Event loop 的每個(gè)階段,都是先執(zhí)行宏任務(wù)隊(duì)列,再執(zhí)行微任務(wù)隊(duì)列
全部宏任務(wù) -> 全部 nextTick 任務(wù) -> 全部微任務(wù)

Node 11.x 及以后的基本流程

Node.js 在升級(jí)到 11.x 后,Event Loop 運(yùn)行原理發(fā)生了變化。一個(gè)宏任務(wù)執(zhí)行完成就執(zhí)行微任務(wù)隊(duì)列,和瀏覽器一致了
宏任務(wù) -> 全部 nextTick 任務(wù) -> 全部微任務(wù) -> 下一個(gè)宏任務(wù) -> 全部 nextTick 任務(wù) -> 全部微任務(wù)

總結(jié)

在 Web 端,Event loop 依賴各個(gè)瀏覽器廠商的實(shí)現(xiàn)
除了正常的宏微任務(wù)外,還擁有獨(dú)特的 UI Rendering 和 MutationObserver
依靠瀏覽器各線程的配合,完成 Event loop 的循環(huán)

而在 Node 端,Event loop 依賴 libuv 的實(shí)現(xiàn),同時(shí)在 Node 11 版本前后有差異
Node 端擁有 6 個(gè)事件階段,每個(gè)階段都可以進(jìn)行 Event loop 循環(huán)

參考文章

Tasks, microtasks, queues and schedules
一次弄懂Event Loop(徹底解決此類面試問(wèn)題)
面試題:說(shuō)說(shuō)事件循環(huán)機(jī)制(滿分答案來(lái)了)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容