JavaScript 事件循環(huán)(含宏任務(wù)與微任務(wù))

本文更新時(shí)間:2019-10-04

JavaScript 特點(diǎn)

JavaScript 是單線程非阻塞的一門(mén)語(yǔ)言。單線程意味著:JavaScript 代碼在執(zhí)行的時(shí)候只有一個(gè)主線程去處理所有的任務(wù),即同一時(shí)間只能做一件事情。非阻塞則表示:當(dāng)執(zhí)行到一項(xiàng)異步任務(wù)的時(shí)候,主線程會(huì)掛起當(dāng)前這個(gè)異步任務(wù),然后在異步任務(wù)返回結(jié)果的時(shí)候再跟進(jìn)一定的規(guī)則去執(zhí)行相應(yīng)的回調(diào)。

思考: 為什么 JavaScript 要設(shè)計(jì)成單線程?
單線程是必要的,也是 Javascript 這門(mén)語(yǔ)言的基石,原因之一在其最初也是最主要的執(zhí)行環(huán)境——瀏覽器中,我們需要進(jìn)行各種各樣的 DOM 操作。試想一下 如果 Javascript 是多線程的,那么當(dāng)兩個(gè)線程同時(shí)對(duì) DOM 進(jìn)行一項(xiàng)操作,例如一個(gè)向其添加事件,而另一個(gè)刪除了這個(gè) DOM,此時(shí)該如何處理呢?因此,為了保證不會(huì) 發(fā)生類似于這個(gè)例子中的情景,Javascript 選擇只用一個(gè)主線程來(lái)執(zhí)行代碼,這樣就保證了程序執(zhí)行的一致性。

事件循環(huán)(Event Loop)

JavaScript 如何實(shí)現(xiàn)非阻塞呢?沒(méi)錯(cuò),就是通過(guò)事件循環(huán)的實(shí)現(xiàn)的。而事件循環(huán)是通過(guò)任務(wù)隊(duì)列機(jī)制協(xié)調(diào)的。

在事件循環(huán)中,每進(jìn)行一次循環(huán)操作稱為 tick,每一次 tick 的任務(wù)處理是比較復(fù)雜的,主要步驟如下:

  • 在本次 tick 中選擇最先進(jìn)入隊(duì)列的任務(wù),如有則執(zhí)行一次;
  • 檢查是否存在微任務(wù)(micro-task),如有則執(zhí)行,直至清空 Microtask Queue;
  • 更新 render;
  • 主線程重復(fù)執(zhí)行上述步驟。

tick 需要了解的是:

  • JS 任務(wù)分為同步任務(wù)和異步任務(wù);
  • 同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧;
  • 主線程之外,事件觸發(fā)線程管理著一個(gè)任務(wù)隊(duì)列,只要異步任務(wù)有了結(jié)果,就在任務(wù)隊(duì)列里面放置一個(gè)事件
  • 一旦執(zhí)行棧中所有同步任務(wù)執(zhí)行完畢(JS 引擎空閑之后),就會(huì)去讀取任務(wù)隊(duì)列,將可運(yùn)行的異步任務(wù)添加到可執(zhí)行棧里面,開(kāi)始執(zhí)行。

任務(wù)有同步任務(wù)、異步任務(wù),而異步任務(wù)又分為宏任務(wù)(macro-task)微任務(wù)(micro-task)

宏任務(wù)(macro-task)
可以理解成:每次執(zhí)行棧的代碼就是一個(gè)宏任務(wù)(包括每次從事件隊(duì)列中獲取的一個(gè)事件回調(diào)并放到執(zhí)行中執(zhí)行)。
瀏覽器為了能夠使得 JS 內(nèi)部 macro-task 與 DOM 任務(wù)有序地執(zhí)行,會(huì)在宏任務(wù)執(zhí)行結(jié)束之后,在下一個(gè)宏任務(wù)開(kāi)始執(zhí)行之前,對(duì)頁(yè)面進(jìn)行重新渲染。

macro-task -> render -> macro-task -> ...

宏任務(wù)包括:
setInterval
setTimeout
setImmediate(node.js)
XHR 回調(diào)
事件回調(diào)(鼠標(biāo)鍵盤(pán)事件)
indexedDB 數(shù)據(jù)庫(kù)等 I/O 操作
UI rendering

微任務(wù)(micro-task)
可以理解成:當(dāng)前 task 執(zhí)行結(jié)束之后立即執(zhí)行的任務(wù)。(在下一個(gè) task 之前 ,在渲染之前)。

macro-task -> micro-task -> render -> macro-task -> ...

微任務(wù)包括:
Promise.then catch finally
process.nextTick(node.js)
MutationObserver
Object.observe(已被棄用)

運(yùn)行機(jī)制

  • 執(zhí)行一個(gè)宏任務(wù)(執(zhí)行棧中沒(méi)有就從事件列表中獲取)
  • 執(zhí)行過(guò)程中如果遇到微任務(wù),就將其添加到微任務(wù)的任務(wù)隊(duì)列里面;
  • 宏任務(wù)執(zhí)行完畢之后,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列里面的所有微任務(wù)(依次執(zhí)行)
  • 當(dāng)前宏任務(wù)執(zhí)行完畢之后,開(kāi)始檢查渲染,然后 GUI 線程接管渲染(但是 UI render 不一定會(huì)執(zhí)行,因?yàn)樾枰紤] UI 渲染消耗的性能已經(jīng)有沒(méi)有 UI 變動(dòng))
  • 渲染完畢后,JS 線程繼續(xù)接管,開(kāi)始下一個(gè)宏任務(wù)(從事件隊(duì)列中獲取)
  • JS 不斷重復(fù)以上步驟,直至所有任務(wù)執(zhí)行完畢。(棧內(nèi)存溢出也會(huì)終止執(zhí)行)

參考:

  1. JS 事件循環(huán)
  2. 詳解 JavaScript 中的 Event Loop(事件循環(huán))機(jī)制(含 node 環(huán)境下的事件循環(huán)機(jī)制)
  3. JS 宏任務(wù)和微任務(wù)
  4. JavaScript 運(yùn)行機(jī)制詳解:再談 Event Loop(阮一峰)
  5. 深入解析 Event Loop 和瀏覽器渲染、幀動(dòng)畫(huà)、空閑回調(diào)的關(guān)系
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容