此為 event loop 系列文章 - 第 2 篇:
- Javascript 中的 event loop 及瀏覽器渲染機制
- 從程式碼角度來看 event loop
- 使用原生的 queueMicrotask 處理微任務
- Vue.nextTick() 中的 event loop
前言
上一篇介紹了 event loop 的運行原理,這篇文章希望藉由各種範例進一步體會 event loop 的執行順序
各種不同的 event loop 範例
1. 基本的 event loop 執行順序
1 | setTimeout(() => { |
步驟:
- 第 1 行的
setTimeout
callback
會放入 宏任務佇列 (macrotask queue) 裡,等待之後執行 - 第 5 行的
promise
callback
會放入 微任務佇列 (microtask queue) 裡,等待之後執行 - 第 9 行的
main script
同步執行,印出 main script - event loop 挑出當輪 微任務佇列 (microtask queue) 的所有 微任務 (microtask) ,執行步驟 2. 的
callback
,印出 promise - 微任務佇列 (microtask queue) 為空,進入下一輪的 event loop 循環
- 此輪從 宏任務佇列 (macrotask queue) 挑出步驟 2. 的 宏任務 (macrotask) 執行,印出 timeout
結果:
1 | 'main script' |
2. 基本的 promise 執行順序
1 | const promise = new Promise((resolve, reject) => { |
步驟:
- 首先執行第 1 行的
new Promise
- 第 2 行印出 1
- 第 3 行將
promise
的狀態設為resolved
,值為success
- 繼續往下執行
promise
裡的程式,遇到第 4 行印出 2 - 第 6 行
promise
的狀態已經為resolved
,將then
後面的callback
放入 微任務佇列 (microtask queue) 中 - 執行第 9 行,印出 4
- event loop 挑出當輪 微任務佇列 (microtask queue) 的所有 微任務 (microtask) => 執行步驟 5. 的
callback
- 執行第 7 行,印出 3
結果:
1 | 1 |
3. async await 寫法的 promise
1 | async function async1() { |
分析
由於 async await
是 promise
的語法糖,所以以下兩種寫法是等價的:
1 | async function async2() { |
1 | const async2 = new Promise((resolve) => { |
步驟:
- 執行第 9 行
async1
函式 - 進入
async1
函式,執行第 2 行,印出 async1 start - 執行第 3 行
async2
函式 - 進入
async2
函式,執行第 7 行,印出 async2 async2
函式結束回傳Promise {<fulfilled>: undefined}
- 回到第 3 行
async2
函式,將第 3 行後的程式放入 微任務佇列 (microtask queue) 中 async1
函式結束,回到第 9 行- 執行第 10 行,印出 start
- event loop 挑出當輪 微任務佇列 (microtask queue) 的所有 微任務 (microtask) => 執行步驟 6. 的程式
- 執行第 4 行,印出 async1 end
結果:
1 | 'async1 start' |
4. 在 promise 中添加長時間的任務執行
1 | setTimeout(function () { |
步驟:
- 執行第 1 行將
setTimeout
的callback
放入 宏任務佇列 (macrotask queue) 中 - 執行第 5 行的
promise
- 執行第 6 行,印出 1
- 第 7-9 行,執行一段長時間的程式後第 8 行將
promise
resolve
- 執行第 10 行,印出 2
- 將第 11 行
then
後的callback
放入 微任務佇列 (microtask queue) 中 - 執行第 15 行,印出 3
- event loop 挑出當輪 微任務佇列 (microtask queue) 的所有 微任務 (microtask) => 執行步驟 6. 的
callback
- 執行第 12 行,印出 5
- 當輪 event loop 結束
- event loop 挑出下一輪 宏任務佇列 (macrotask queue) 中的第一個 宏任務 (macrotask) => 執行步驟 1. 的
callback
- 執行第 2 行,印出 4
結果:
1 | 1 |
5. promise 與 setTimeout 的互相執行
1 | Promise.resolve().then(() => { |
步驟:
- 首先第 1 行將
promise.resolve
後的callback
放入 微任務佇列 (microtask queue) 中 - 執行第 7 行將
timer1
的callback
放入 宏任務佇列 (macrotask queue) 中 - 執行第 13 行,印出 start
- event loop 挑出當輪 微任務佇列 (microtask queue) 的所有 微任務 (microtask) => 執行步驟 1. 的
callback
- 執行第 2 行,印出 promise1
- 執行第 3 行將
timer2
的callback
放入 宏任務佇列 (macrotask queue) 中 - 當輪 event loop 結束
- event loop 挑出下一輪 宏任務佇列 (macrotask queue) 中的第一個 宏任務 (macrotask) => 執行步驟 2. 的
callback
- 執行第 8 行,印出 timer1
- 執行第 9 行將
promise.resolve
後的callback
放入 微任務佇列 (microtask queue) 中 - event loop 挑出當輪 微任務佇列 (microtask queue) 的所有 微任務 (microtask) => 執行步驟 10. 的
callback
- 執行第 10 行,印出 promise2
- 當輪 event loop 結束
- event loop 挑出下一輪 宏任務佇列 (macrotask queue) 的所有 宏任務 (macrotask) => 執行步驟 6. 的
callback
- 執行第 4 行,印出 timer2
結果:
1 | 'start' |
不同狀況下的 event loop 範例
以上範例單純只牽涉到 宏任務 (macrotask) 與 微任務 (microtask) 時,根據 event loop 的規則,執行的順序會是固定的,但如果牽涉到頁面的渲染,使用到 requestAnimationFrame
時,執行的順序就不是那麼絕對的了,以下我們來看看這些狀況:
1. setTimeout 與 requestAnimationFrame
1 | setTimeout(() => { |
首先這一段程式執行後,畫面最後印出來的會是 A, B, C 或是 D 呢?

先貼一張 Javascript 中的 event loop 及瀏覽器渲染機制 來複習 event loop 循環的全貌,以上程式使用到的 requestAnimationFrame
會在每一幀渲染畫面 之前 執行,所以我們會認為它執行的順序是:
步驟:
- 第 1 行的
setTimeout
callback
會放入 宏任務佇列 (macrotask queue) 裡,等待之後執行 - 第 6 行的
requestAnimationFrame
callback
會等待下一次畫面渲染前執行 - 第 11 行的
setTimeout
callback
會放入 宏任務佇列 (macrotask queue) 裡,等待之後執行 - 執行第 16, 17 行
- 接著當輪的 宏任務佇列 (macrotask queue) 都執行完了,所以會看 微任務佇列 (microtask queue) 中有沒有 微任務 (microtask) 要執行
- 因爲程式碼中沒有 微任務 (microtask) => 微任務佇列 (microtask queue) 為空 => 沒有 微任務 (microtask) 需要處理
- 預備進行畫面渲染,執行第 6 行的
requestAnimationFrame
- 進入 Style => Layout => Paint => Composite 這些階段的頁面渲染
- 當輪 event loop 結束
- 下一輪的 event loop 從 宏任務佇列 (macrotask queue) 挑出 宏任務 (macrotask) 執行 => 執行步驟 1. 的
setTimeout
callback
- 此輪的 event loop 已經沒有後續任務了,當輪 event loop 結束
- 再下一輪的 event loop 從 宏任務佇列 (macrotask queue) 挑出 宏任務 (macrotask) 執行 => 執行步驟 3. 的
setTimeout
callback
結果:
由以上步驟來看最後執行的應該是第 13 行,所以最後畫面會顯示的是 C,但當你把這段程式碼多次執行時,會發現最後顯示的不一定是 C,有時候畫面上顯示的會是 B,這是為什麼呢?
關鍵就在於瀏覽器什麼時候渲染畫面,上一篇文章 event loop 如何安排任務的執行順序 中提到在 微任務 (microtask) 都執行完後,下一步會判斷是否需要渲染 UI 畫面,而這就是所謂的關鍵點了
因此當瀏覽器判斷第一輪執行 event loop 後,如果還不需要渲染畫面,那麼也就不會執行步驟 2. requestAnimationFrame
裡的 callback
,這種狀況下,步驟 9. 及步驟 11. 就會先被執行,最後瀏覽器判斷需要渲染畫面時才會執行步驟 2.,因此就會看到畫面上最後顯示的是 B
2. 被延遲執行的 requestAnimationFrame
上一個範例我們看到瀏覽器可能會自行判斷在當下的 event loop 結束後,是否需要進行渲染畫面,因此也導致 requestAnimationFrame
的執行時機不確定,第 2 個範例我們來看看如果在 requestAnimationFrame
執行之前有耗時較長的任務需要處理,會有什麼樣的結果
1 | const longTask = (ms = 500) => { |
首先這裡我們寫了一個 longTask
的函式,模擬長時間的運算(500ms),這裡我們打算在每個 宏任務 (macrotask) 執行後,再執行一次 longTask
,這樣會導致每輪的 event loop 結束後都經過了 500ms,而這時間遠遠超過一幀 (17ms) 瀏覽器判斷應該進行畫面渲染的時機,所以每輪 宏任務 (macrotask) 執行後瀏覽器都會渲染畫面
順序:
- 首先執行第 29-31 行,因為 31 行添加了一個長時間的任務,可以保證瀏覽器在執行 31 行後會重新渲染畫面
- 在渲染畫面前會執行第 18-21 行的
requestAnimationFrame
callback
,第 20 行會將接下來的畫面印出 B - 執行第 12-16 行,因為 15 行添加了一個長時間的任務,所以瀏覽器會執行渲染,這一輪的畫面將會印出 A
- 執行第 23-27 行,因為 26 行添加了一個長時間的任務,所以瀏覽器會執行渲染,這一輪的畫面將會印出 C
結果:
- 因為每輪 event loop 都添加了 500ms 的長時間任務,所以每輪 event loop 執行後瀏覽器會判斷需要渲染畫面,可以明顯地看到 B => A => C 依序印出
- 第 30 行其實沒有任何作用,因為在頁面渲染前會執行到第 20 行,將原本第 30 行覆蓋過去
- 第 19 行可以看到
requestAnimationFrame
的執行時機在長時間任務執行下,有可能會被延遲到 500ms 後,不會再是每 17ms 都執行一次
參考資料
要就来45道Promise面试题一次爽到底
這篇文章的大部分範例都引用這裡,看完這裡面的 45 道題後,一定對於整個 event loop 的執行順序更為熟悉Understanding the browser’s Event Loop for building high-performance web applications. Part 1.
Which queue is associated with requestAnimationFrame?
宏任務(microtask) 有 macrotask queue,微任務(microtask) 有 macrotask queue,但 requestAnimationFrame 的 callback 會被放在哪裡等待後續執行呢?