EventLoop
前言
- 因為 JavaScript 是單執行緒,一次只能做一件事
- 但藉助瀏覽器的 Web APIs,具有非同步的特性 (例如 XMLHttpRequest、Fetch API、setTimeout 等)
- 為了主執行緒能夠不被阻塞地繼續執行其它任務,使用 Event Loop 機制,來實現非同步操作
非同步執行過程由以下區塊組成:
1. Call Stack
- 後進先出 (Last In First Out)
- 每調用一次函式,函式都會被放進 Call Stack
- 只要正在執行的函式裡調用了新的函式,新函式也會被放入 Call Stack
- 函式執行完成,會被清出,直到 call stack 被清空
- 超過 Call Stack 容量的情況,稱作 Stack overflow
範例 1
const multiply = (x, y) => x*y;const square = (x) => multiply(x,x);const isRightTriangle = (a, b, c){ return square(a) + square(b) === square(c);};isRightTriangle(3, 4, 5);//Call Stack 放入順序//3. multiply(3,3)//2. square(3)//1. isRightTriangle(3,4,5)//Call Stack執行順序//1. multiply(3,3)//2. square(3)//3. isRightTriangle(3,4,5)
範例2
const fn1 = () => { console.log('hello1')}const fn2 = () => { fn1()}const fn3 = () => { fn2()}fn3()//Call Stack 放入順序//3. fn1//2. fn2//1. fn3//Call Stack執行順序//1. fn1//2. fn2//3. fn3
範例:當遞迴函式沒有終止條件或終止條件設置不當時,就會造成 call stack overflow
function hi() { hi()}hi()
2. Web APIs
- 瀏覽器提供的 API,例如常被使用的 setTimeout
- 並不是 JavaScript 引擎的一部分
- 非同步的任務會在這裡執行完成後把結果或 callback function 傳到 Task Queue 等待
- 常搭配 callback function 使用
3. Task Queue(Callback Queue)
- 先進先出(First In First Out )的工作佇列
- 專門接收從 Web APIs 傳來的任務結果 (非同步的函式)
4. Event Loop
- 判斷 Call Stack 是否已清空
- 如果是的話把 Task Queue 裡面的任務依序丟回 Call Stack 當中執行
目的:
- 之所以能夠實踐異步,正是因為有事件循環 (Event loop) 的機制
- 透過事件循環機制,能有效解決 JavaScript 單執行緒的問題,讓耗時的操作不會阻塞主線程
範例:同步與非同步函式執行順序
console.log('start')setTimeout(() => { console.log('code after 2s')}, 2000)console.log('end')// start// end// code after 2s//1. console.log('start')被放入Call Stack當中並執行印出 `'start'`//2. setTimeout被放入Task Queue中等待Call Stack中的任務全部執行完畢//3. console.log('end')被放入Call Stack當中並執行印出 `'end'`//4. Call Stack清空,Task Queue的任務放入Call Stack內並執行印出`'code after 2s'`
執行示意圖:
異步任務的種類:
任務優先度:
- 同步任務 > 微任務 > 宏任務
1. 微任務 Microtask
在 JS 引擎執行
- 包含哪些:
- Promise.then() catch()
- MutationObserver ( 監聽 DOM tree 變動的 API )
- process.nextTick ( 屬於 Node.js 的 Event Loop )
- Async/Await
2. 宏任務 Macrotask
在瀏覽器或 Node 環境執行
- 包含哪些: :
- Web APIs
- I/O ( 讀寫、存取 )
- 載入 JS 檔案並且執行時,例如 <script>
- 渲染畫面 ( Render )
- settimeout, setinterval
執行順序
- 執行一次宏任務 (最開始會是整個 srcipt )
- 執行過程中如果遇到宏任務,就放進宏任務隊列
- 執行過程中如果遇到微任務,就放進微任務隊列
- 當執行棧空了,先檢查微任務隊列,如果有微任務,就依序執行直到微任務隊列為空
- 接著進行瀏覽器的渲染,渲然完後開始下一個宏任務 (回到最開始的步驟)
範例
<script> console.log('同步任務開始') // 同步任務 setTimeout(() => { console.log('宏任務 1') // 宏任務 }, 0) Promise.resolve().then(() => { console.log('微任務 1') // 微任務 }) console.log('同步任務結束') // 同步任務 setTimeout(() => { console.log('宏任務 2') // 宏任務 }, 0)</script>
// 輸出結果// 同步任務開始// 同步任務結束// 微任務 1// 宏任務 1// 宏任務 2
- 執行主腳本作為宏任務:
- 首先,整個 <script> 標籤中的內容被視為一個宏任務</script>
- 在這個宏任務中,JavaScript 引擎會按順序執行腳本中的同步程式碼
- 執行同步任務:
- 腳本中的同步程式碼(例如
console.log
)會被立即執行
- 安排微任務和宏任務:
- 在這個宏任務(即主腳本)的過程中,會遇到非同步操作(如
Promise.then
和setTimeout
) - 這些非同步操作不會立即執行,而是會安排相應的微任務(
Promise.then
)和宏任務(setTimeout
)
- 完成宏任務,處理微任務佇列:
- 主腳本(初始宏任務)執行完畢後,JavaScript 引擎會處理所有排隊的微任務
- 這時,
Promise.then callback
會被執行。
- 執行下一個宏任務:
- 微任務佇列清空後,事件迴圈會處理下一個宏任務
setTimeout callback
(如果其延遲時間已到)將被執行。
參考資料
# JavaScript