Event Loop 那些事兒

Event Loop 那些事兒

咱們一般說 JavaScript 是單線程的,其實是指在 JS 引擎中負責解釋和執行 JS 代碼的線程只有一個,通常成爲主線程,在這種前提下,爲了讓用戶的操做不存在阻塞感,前端 APP 的運行須要依賴於大量的異步過程,因此固然瀏覽器中還存在一些其餘的線程,好比處理 http 請求的線程、處理 DOM 事件的線程、定時器線程、處理文件讀寫的 I/O 線程等等。前端

異步過程

一個異步過程一般是這樣的:web

  1. 主線程發起一個異步請求,相應的工做線程接收請求並告知主線程已收到;
  2. 主線程能夠繼續執行後面的代碼,同時工做線程執行異步任務;
  3. 工做線程完成工做後通知主線程,主線程收到通知後調用回調函數。

消息隊列和事件循環

異步過程當中,工做線程在異步操做完成後須要通知主線程,那麼這個通知機制是怎樣實現的呢?答案是利用消息隊列和事件循環。消息隊列是一個先進先出的隊列,裏面存放着各類消息,咱們能夠簡單的理解爲消息就是註冊異步任務時添加的回調函數;事件循環是指主線程重複從消息隊列中獲取消息、執行回調的過程,之因此稱爲事件循環,就是由於它常常被用相似以下方式來實現:promise

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

消息隊列是一個存儲着待執行任務的隊列,其中的任務嚴格按照時間前後順序執行,排在隊頭的任務將會率先執行,而排在隊尾的任務會最後執行。消息隊列每次僅執行一個任務,在該任務執行完畢以後,再執行下一個任務。執行棧則是一個相似於函數調用棧的運行容器,當執行棧爲空時 JS 引擎便檢查消息隊列,若是不爲空消息隊列便將第一個任務壓入執行棧中運行。瀏覽器

下面咱們來看下述代碼來驗證咱們的想法:異步

setTimeout(function() {
    console.log(4)
}, 0);

new Promise(function(resolve) {
    console.log(1)

    for (var i = 0; i < 10000; i++) {
        i == 9999 && resolve()
    }

    console.log(2)
}).then(function() {
    console.log(5)
});

console.log(3);

比較弔詭的事情出現了,爲何結果是「1, 2, 3, 5, 4」,而不是「1, 2, 3, 4, 5」呢?!

按道理來講,執行 setTimeout 時由於延遲爲0,因此 console.log(4) 直接插入至消息隊列;建立 Promise 實例時同步執行其函數體內的代碼,先打印 1,再循環10000次後執行 resolve 將 then 中的回調函數 console.log(5) 插入至消息隊列,而後打印 2;最後執行 console.log(3) 打印 3;在主線程執行完成後讀取消息隊列,依次打印4和5函數

上面的想法固然是比較天真的,實際上瀏覽器中僅有一個事件循環,而後消息隊列是能夠有多個的。

macro-queue: script (總體代碼), setTimeout, setInterval, setImmediate, I/O, UI Rendering
micro-queue: process.nextTick, Promise, Object.observe, MutationObserveroop

而且 micro-queue 的任務優先級高於 macro-queue 的任務優先級,這兩個任務隊列執行順序以下:取1個 macro-task 執行之,而後把全部 micro-task 順序執行完,再取 macro-task 中的下一個任務,以此類推依次進行。線程

優先級:process.nextTick > promise.then > setTimeout > setImmediate

Tip:process.nextTick 永遠大於 promise.then 緣由其實很簡單:在 NodeJS 中,_tickCallback 在每一次執行完 TaskQueue 中的一個任務後被調用,而在這個_tickCallback 中實質上幹了兩件事:code

  1. 執行掉 nextTickQueue 中全部任務
  2. 第一步執行完後,執行 _runMicrotasks 函數(執行 micro-task 中的部分,即 promise.then 註冊的回調)

總結:瀏覽器環境通常只能有一個事件循環(實際上有兩類:browsing contexts 和 web workers),而一個事件循環能夠多個任務隊列,每一個任務都有一個任務源。相同任務源的任務,只能放到一個任務隊列中;不一樣任務源的任務,能夠放到不一樣任務隊列中。舉個栗子,客戶端可能實現一個包含鼠標鍵盤事件的任務隊列,還有其餘的任務隊列,而給鼠標鍵盤事件的任務隊列更高優先級,例如75%的可能性執行它,這樣就能保證流暢的交互性,並且別的任務也能執行到。server

至此,再返回去看以前的代碼就不難分析出:代碼執行開始把 setTimeout 的回調插入至 macro-queue 中,而打印完1後把 promise.then 的回調函數插入至 micro-queue 中,總體代碼執行完後,按照消息隊列的優先級,先執行 micro-task 即打印5,最後執行 macro-task 即打印4。

相關文章
相關標籤/搜索