事件循環Event loop究竟是什麼

摘要:本文經過結合官方文檔MDN和其餘博客深刻解析瀏覽器的事件循環機制,而NodeJS有另外一套事件循環機制,不在本文討論範圍中。process.nextTick和setImmediate是NodeJS的API,因此本文也不予討論。

首先,先了解幾個概念。javascript

Javascript究竟是單線程仍是多線程語言?


Javascript是一門單線程語言。相信應該有很多朋友對於Javascript是單線程語言還有些疑問(題外話:以前在某次面試中遇到一個面試官,一來就是「咱們知道JS是一門多線程語言。。。」巴拉巴拉,當時就把我給愣住了。),不是有Web Worker能夠建立多個線程嗎?答案就是,Javascript是單線程的,可是他的運行環境不是單線程。要如何理解這句話,首先得從Javascript運行環境好比瀏覽器的多線程提及。 html

瀏覽器一般包含如下線程:java

  1. GUI渲染線程

    • 主要負責頁面的渲染,解析HTML、CSS,構建DOM樹,佈局和繪製等。
    • 當界面須要重繪或者因爲某種操做引起迴流時,將執行該線程。
    • 該線程與JS引擎線程互斥,當執行JS引擎線程時,GUI渲染會被掛起。
  2. JS引擎線程

    • 該線程負責處理Javascript腳本,執行代碼。
    • 負責執行待執行的事件,好比定時器計數結束,或者異步請求成功並正確返回時,將依次進入任務隊列,等待JS引擎線程執行。
    • 該線程與GUI線程互斥,當JS線程執行Javascript腳本事件過長,將致使頁面渲染的阻塞。
  3. 定時器觸發線程

    • 負責執行異步定時器一類函數的線程,如:setTimeout,setInterval。
    • 主線程依次執行代碼時,遇到定時器會將定時器交給該線程處理,當計數完畢後,事件觸發線程會將計數完畢的事件回調加入到任務隊列,等待JS引擎線程執行。
  4. 事件觸發線程

    • 主要負責將等待執行的事件回調交給JS引擎線程執行。
  5. 異步http請求線程

    • 負責執行異步請求一類函數的線程,如:Promise,axios,ajax等。
    • 主線程依次執行代碼時,遇到異步請求,會將函數交給該線程處理,當監聽到狀態碼變動,若是有回調函數,事件觸發線程會將回調函數加入到任務隊列,等待JS引擎線程執行。

Web Worker是瀏覽器爲Javascript提供的一個能夠在瀏覽器後臺開啓一個新的線程的API(相似上面說到瀏覽器的多個線程),使Javascript能夠在瀏覽器環境中多線程運行,但這個多線程是指瀏覽器自己,是它在負責調度管理Javascript代碼,讓他們在恰當時機執行。因此Javascript自己是不支持多線程的。 ios

異步


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

  1. 主線程發起一個異步請求,異步任務接受請求並告知主線程已收到(異步函數返回);
  2. 主線程繼續執行後續代碼,同時異步操做開始執行;
  3. 異步操做執行完成後通知主線程;
  4. 主線程收到通知後,執行異步回調函數。

這個過程有個問題,異步任務各任務的執行時間過程長短不一樣,執行完成的時間點也不一樣,主線程如何調控異步任務呢?這就引入了消息隊列。面試

棧、堆、消息隊列


:函數調用造成的一個由若干幀組成的棧。ajax

:對象被分配在堆中,堆是一個用來表示一大塊(一般是非結構化的)內存區域。axios

消息隊列:一個Javascript運行時包含了一個待處理消息的消息隊列。每個消息都關聯着一個用來處理這個消息的回調函數。在事件循環期間,運行時會從最早進入隊列的消息開始處理,被處理的消息會被移出隊列,並做爲輸入參數來調用與之關聯的函數。而後事件循環在處理隊列中的下一個消息。api

事件循環Event loop


瞭解了上述要點,如今回到主題事件循環。那麼Event loop究竟是什麼呢?promise

Event loop是一個執行模型,在不一樣的地方有不一樣的實現。瀏覽器和NodeJS基於不一樣的技術實現了各自的Event loop。
如今明白爲何要把NodeJS排除在外了吧?一樣網上不少Event loop的相關博文一來就是Javascript的Event loop,實際上說的都是瀏覽器的Event loop。
瀏覽器的Event loop是在Html5規範中定義的,大體總結以下:

一個事件循環裏有不少個任務隊列(task queues)來自不一樣任務源,每個任務隊列裏的任務(task)都是嚴格按照先進先出的順序執行的,可是不一樣任務隊列的任務執行順序是不肯定的,瀏覽器會本身調度不一樣任務隊列。也有地方把task稱之爲macrotask(宏任務)。

規範中還提到了microtask(微任務)的概念,如下是規範闡述的進程模型:

  1. 選擇當前要執行的任務隊列,選擇一個最早進入任務隊列的任務,若是沒有任務能夠選擇,則會跳轉至microtask的執行步驟;
  2. 將事件循環的當前運行任務設置爲已選擇的任務;
  3. 運行任務;
  4. 將事件循環的當前任務設置爲null,將運行完的任務從任務隊列中移除;
  5. microtask步驟:進入microtask檢查點;
  6. 更新界面渲染;
  7. 返回第一步。

執行進入microtask檢查點時,用戶代理會執行如下步驟:

  1. 設置進入microtask檢查點的標誌爲true;
  2. 當事件循環的微任務隊列不爲空時:選擇一個最早進入microtask隊列的microtask,設置事件循環當前運行任務爲此microtask;
  3. 運行microtask;
  4. 設置事件循環當前運行任務爲null,將運行結束的microtask從microtask隊列中移除;
  5. 對於相應事件循環的每一個環境設置對象,通知它們哪些promise爲rejected;
  6. 清理indexedDB的事務;
  7. 設置進入microtask檢查點的標誌爲false。

由上可總結爲:在事件循環中,用戶代理會不斷從task隊列中按順序取task執行,每執行完一個task都會檢查microtask隊列是否爲空(執行完一個task的具體標誌是函數執行棧爲空),若是不爲空則會一次性執行完全部microtask。而後再進入下一個循環去task隊列中取下一個task執行。

task/macrotask(宏任務)

  • script(總體代碼)
  • setTimeout
  • setInterval
  • I/O
  • UI rendering

microtask(微任務)

  • Promise.then catch finally
  • MutationObserver

來看一個例子:

console.log('script start');

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

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

運行結果是:

script start
script end
promise1
promise2
setTimeout

那麼問題來了,不是說每一個事件循環開始會從task隊列取最早進入的task執行,而後再執行全部microtask嗎?爲何setTimeout是task卻在Promise.then這個task的前面呢?反正我一開始是有這個疑惑的,不少文章都沒有說清楚這個具體執行的順序,大部分都是在描述規範的時候說的是「每一個事件循環開始會從task隊列中取一個task執行,而後再執行全部microtask」,可是也有部分文章說的是「每一個事件循環開始都是先執行全部microtask」。通過本人多方查證,規範裏的描述如上確實就是每一個事件循環都是先執行task,那爲何上面例子裏面體現出來的是先執行全部microtask呢?

script(總體代碼)屬於task。

來看一下上面例子的詳細執行過程:

  1. 事件循環開始,task隊列中只有一個script,選擇script做爲事件循環的已選擇任務;
  2. script按順序執行,同步代碼直接輸出(script start、script end);
  3. 遇到setTimeout,0ms後將回調函數放入task隊列;
  4. 遇到Promise,將第一個then的回調函數放入microtask隊列;
  5. 當全部script代碼執行完成後,此時函數執行棧爲空,開始檢查microtask隊列,隊列只有第一個.then的回調函數,執行輸出「promise1」,因爲第一個.then返回的依然是promise,因此第二個.then的回調會放入microtask隊列繼續執行,輸出「promise2」;
  6. 此時microtask隊列空了,進入下一個事件循環,檢查task隊列取出setTimeout回調函數,執行輸出「setTimeout」,代碼執行完成。

這樣是否是清楚了?因此實際上一開始執行script代碼的時候就已經開始事件循環了,這就解釋了爲何好像每次都是先執行全部的microtask。同時,這個例子中還引伸出一個要點:在執行microtask任務的時候,若是又產生了新的microtask,那麼會繼續添加到隊列的末尾,且也會在這個事件循環週期執行,直到microtask隊列爲空爲止。

相關文章
相關標籤/搜索