老是被問事件循環,今天終於懂了!


在瀏覽器中,JavaScript 的執行是單線程的。如何在單線程中實現異步操作呢?答案就是事件循環。

事件循環(Event Loop)

瀏覽器通過事件循環來處理事件、用戶交互、JS 代碼執行、渲染、網絡請求等。通常又兩種事件循環,一種是 Window 事件循環,一種是 Worker 事件循環。由於它們核心的工作原理相同,本文我們僅僅討論 Window 事件循環。

事件循環,首先是一個循環,每個循環周期會執行一些代碼,一個循環周期被稱為 tick

while (eventLoop.waitForTask()) {
  eventLoop.processNextTask()
}

一個事件循環有一到多個任務隊列。每個任務隊列就是一個有序的任務列表。可以理解為一段要執行的代碼,或者瀏覽器要執行的一個動作,比如發送事件、解析 HTML 等。

網頁和瀏覽器本身的用戶界面程序運行在相同的線程中,共享相同的事件循環。 該線程就是主線程,它除了運行網頁本身的代碼之外,還負責收集和派發用戶和其它事件,以及渲染和繪制網頁內容等。然后,事件循環會驅動發生在瀏覽器中與用戶交互有關的一切。

執行過程

簡略的說,事件循環在每一個循環周期都會順序執行下面的步驟:

  1. 選擇一個任務隊列,從隊列中取出最靠前(最老的)的任務。如果已經沒有任務了,則跳到第 3 步。
  2. 執行取出的任務。
  3. 從微任務(Micro Task)隊列中取出微任務執行,知道清空微任務隊列。
  4. 更新渲染(resize、scroll、動畫等)
  5. 返回第 1 步。

每一個時間循環都有一個微任務隊列。微任務隊列與任務隊列很像,不同的地方在於,每次循環周期只會執行任務隊列中的一個任務,在這期間產生的任何任務都只能在下一個循環周期中才能得以執行。在執行前任務后,事件循環會一次執行微任務隊列中的每一個微任務,直到微任務隊列為空。也就是說,在微任務執行過程中新產生的微任務,也會在當前循環周期內得到執行。

不同的任務隊列可能有不同的優先級。比如瀏覽器可能會將用戶鼠標和鍵盤輸入(用戶交互)的任務都放在一個任務隊列中,其他任務放到另外一個隊列中。在每個循環周期中優先從用戶交互隊列中取出任務執行,來保證及時響應用戶操作。

下圖展示了一個事件循環周期的執行過程。

事件循環

任務與微任務

一個任務可以簡單的理解為一段要執行的 JavaScript 代碼。比如當執行 <script> 標簽中的代碼時,一個任務會被添加到任務隊列中。事件的回調函數、setTimeoutsetInterval 的回調函數都會作為任務放到任務隊列中。

每個事件循環周期,事件循環會從任務隊列中取出一個最老的任務執行,其他任務要等到下一個事件循環周期才會執行。

微任務與任務沒有本質的區別,只是因為被放入的微任務任務隊列。事件循環在每個循環周期都會清空微任務隊列中的任務。

在瀏覽器的實現中,setTimeoutsetInterval 被放在了任務隊列中,PromiseMutation Observer API 被放在的微任務隊列中。我們也可以借助於 queueMicrotask() 函數向微任務隊列中添加任務。

執行異步代碼

我們有很多種方式來執行異步代碼。

回調函數

我們可以給一些事件添加監聽,設置回調函數,從而在觸發事件的時候執行函數。

buttonEl.addEventListener('click', () => { /* 點擊響應 */ });

回調函數會被放到任務隊列中執行。

setTimeout 和 setInterval

這兩個函數大家都非常熟悉,setTimeout 會在指定的時間之后執行回調函數,setInterval 會每個一定的時間執行一次回調函數。這些回調函數都是以任務的形式被添加到任務隊列中。

setTimeout(() => {
  /* 1s 后執行 */
}, 1000);

setInterval(() => {
  /* 每 500 ms 執行一次 */
}, 500);

注意,這兩個函數都接收一個時間參數。但是實際運行的時候,事件循環並不會保證一定按照這個時間執行。

requestAnimationFrame

requestAnimationFrame 是一個特殊的工具函數,瀏覽器會在重繪頁面之前調用這個函數設置的回調,允許我們在頁面重繪之前更新頁面。

通過這個函數,我們可以很好的在代碼執行和運行設備的顯示幀率(display frame rate)之間取得一個平衡。

假如,我們通過 setInterval 來控制動畫,由於每個事件循環周期執行的時間不可控,而顯示器的刷新頻率是固定的(通常是 60Hz),因此如果我們的動畫執行過快,會出現掉幀,執行過慢又會有卡頓現象。

requestAnimationFrame 會綜合考慮顯示器的刷新頻率和代碼的執行,以達到動畫能夠順滑執行。

requestAnimationFrame(() => {
  /* 執行一次 */
})

// 每個事件循環都會考慮執行
function alwaysRun() {
  /* 函數邏輯 */
  requestAnimationFrame(alwaysRun);
}
alwaysRun();

Promise 和 Async/await

Promise 和 Async/await 最終都會以 promise 的形式在代碼中執行。

Promise 可以幫助我們管理有依賴關系的任務,同時可以一定程度的避免回調函數的回調地獄問題。

new Promise((resolve, reject) => {
  /* 函數邏輯 */
})
.then()
.then() // 鏈式調用

Async/await 是 Promise 的語法糖,可以幫助我們實現以同步的形式編寫異步代碼,解決了回調地獄問題。

async function task() {
  await someFunc();
  await someOtherFunc();
}

Promise 和 Async/await 的任務都是微任務,會被放到微任務隊列中。

參考資料

獲取常見面試知識點、技術解決方案、教程,可以掃碼關注公眾號“眾里千尋”獲取,或者來這里 https://everfind.github.io


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM