Stories

Detail Return Return

瀏覽器事件循環 - Stories Detail

完整高頻題庫倉庫地址:https://github.com/hzfe/awesome-interview

完整高頻題庫閲讀地址:https://febook.hzfe.org/

相關問題

  • 什麼是瀏覽器事件循環
  • 瀏覽器為什麼需要事件循環
  • Node.js 中的事件循環

回答關鍵點

任務隊列 異步 非阻塞

瀏覽器需要事件循環來協調事件、用户操作、腳本執行、渲染、網絡請求等。通過事件循環,瀏覽器可以利用任務隊列來管理任務,讓異步事件非阻塞地執行。每個客户端對應的事件循環是相對獨立的。

知識點深入

1. 什麼是瀏覽器事件循環

在計算機中,Event Loop 是一個程序結構,用於等待和發送消息和事件。 —— 維基百科

Event Loop 可以理解為一個消息分發器,通過接收和分發不同類型的消息,讓執行程序的事件調度更加合理。

瀏覽器事件循環是以瀏覽器為宿主環境實現的事件調度,操作順序如下:

  1. 執行同步代碼。
  2. 執行一個宏任務(執行棧中沒有就從任務隊列中獲取)。
  3. 執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中。
  4. 宏任務執行完畢後,立即執行當前微任務隊列中的所有微任務(依次執行)。
  5. 當前宏任務執行完畢,開始檢查渲染,然後渲染線程接管進行渲染。
  6. 渲染完畢後,JavaScript 線程繼續接管,開始下一個循環。

下圖展示了這個過程:

image.png

圖片來源 JS CONF EU 2014

2. 瀏覽器為什麼需要事件循環

由於 JavaScript 是單線程的,且 JavaScript 主線程和渲染線程互斥,如果異步操作(如上圖提到的 WebAPIs)阻塞 JavaScript 的執行,會造成瀏覽器假死。而事件循環為瀏覽器引入了任務隊列(task queue),使得異步任務可以非阻塞地進行。

瀏覽器事件循環在處理異步任務時不會一直等待其返回結果,而是將這個事件掛起,繼續執行棧中的其他任務。當異步事件返回結果,將它放到任務隊列中,被放入任務隊列不會立刻執行回調,而是等待當前執行棧中所有任務都執行完畢,主線程處於空閒狀態,主線程會去查找任務隊列中是否有任務,如果有,取出排在第一位的事件,並把這個事件對應的回調放到執行棧中,執行其中的同步代碼。

3. 宏任務與微任務

異步任務被分為兩類:宏任務(macrotask)與微任務(microtask),兩者的執行優先級也有所區別。

宏任務主要包含:script(整體代碼)、setTimeout、setInterval、setImmediate、I/O、UI 交互事件。

微任務主要包含:Promise、MutationObserver 等。

在當前執行棧為空的時候,主線程會查看微任務隊列是否有事件存在。如果不存在,那麼再去宏任務隊列中取出一個事件並把對應的回調加入當前執行棧;如果存在,則會依次執行隊列中事件對應的回調,直到微任務隊列為空,然後去宏任務隊列中取出最前面的一個事件,把對應的回調加入當前執行棧。如此反覆,進入循環。下面通過一個具體的例子來進行分析:

Promise.resolve().then(() => {
  // 微任務1
  console.log("Promise1");
  setTimeout(() => {
    // 宏任務2
    console.log("setTimeout2");
  }, 0);
});
setTimeout(() => {
  // 宏任務1
  console.log("setTimeout1");
  Promise.resolve().then(() => {
    // 微任務2
    console.log("Promise2");
  });
}, 0);

最後輸出順序為:Promise1 => setTimeout1 => Promise2 => setTimeout2。具體流程如下:

  1. 同步任務執行完畢。微任務 1 進入微任務隊列,宏任務 1 進入宏任務隊列。
  2. 查看微任務隊列,微任務 1 執行,打印 Promise1,生成宏任務 2,進入宏任務隊列。
  3. 查看宏任務隊列,宏任務 1 執行,打印 setTimeout1,生成微任務 2,進入微任務隊列。
  4. 查看微任務隊列,微任務 2 執行,打印 Promise2。
  5. 查看宏任務隊列,宏任務 2 執行,打印 setTimeout2。

4. Node.js 中的事件循環

在 Node.js 中,事件循環表現出的狀態與瀏覽器中大致相同。不同的是 Node.js 中有一套自己的模型。 Node.js 中事件循環的實現是依靠的 libuv 引擎。下圖簡要介紹了事件循環操作順序:

image.png

圖片來源 Node.js 官網
  1. timers:本階段執行已經被 setTimeout() 和 setInterval() 的調度回調函數。
  2. pending callbacks:執行延遲到下一個循環迭代的 I/O 回調。
  3. idle、prepare:僅系統內部使用。
  4. poll:檢索新的 I/O 事件;執行與 I/O 相關的回調(幾乎所有情況下,除了關閉的回調函數,那些由計時器和 setImmediate() 調度的之外),其餘情況 node 將在適當的時候在此阻塞。
  5. check:setImmediate() 回調函數在這裏執行。
  6. close callbacks:一些關閉的回調函數,如:socket.on('close', ...)。

在每次運行的事件循環之間,Node.js 檢查它是否在等待任何異步 I/O 或計時器,如果沒有的話,則完全關閉。

需要注意的是,宏任務與微任務的執行順序在 Node.js 的不同版本中表現也有所不同。同樣通過一個具體的例子來分析:

setTimeout(() => {
  console.log("timer1");
  Promise.resolve().then(function () {
    console.log("promise1");
  });
}, 0);

setTimeout(() => {
  console.log("timer2");
  Promise.resolve().then(function () {
    console.log("promise2");
  });
}, 0);
  1. 在 Node.js v11 及以上版本中一旦執行一個階段裏的一個宏任務(setTimeout,setInterval 和 setImmediate),會立刻執行微任務隊列,所以輸出順序為timer1 => promise1 => timer2 => promise2
  2. 在 Node.js v10 及以下版本,要看第一個定時器執行完成時,第二個定時器是否在完成隊列中。

    • 如果第二個定時器還未在完成隊列中,輸出順序為timer1 => promise1 => timer2 => promise2
    • 如果是第二個定時器已經在完成隊列中,輸出順序為timer1 => timer2 => promise1 => promise2

參考資料

  1. whatwg event loops
  2. wikipedia event loops
  3. Node.js event loops
user avatar grewer Avatar cyzf Avatar Leesz Avatar smalike Avatar freeman_tian Avatar front_yue Avatar jingdongkeji Avatar dirackeeko Avatar littlelyon Avatar inslog Avatar anchen_5c17815319fb5 Avatar u_17443142 Avatar
Favorites 150 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.