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'`

執行示意圖:

https://i.imgur.com/wF0TNn8.png

異步任務的種類:

任務優先度:

  • 同步任務 > 微任務 > 宏任務

1. 微任務 Microtask

在 JS 引擎執行

  • 包含哪些:
    1. Promise.then() catch()
    2. MutationObserver ( 監聽 DOM tree 變動的 API )
    3. process.nextTick ( 屬於 Node.js 的 Event Loop )
    4. Async/Await

2. 宏任務 Macrotask

在瀏覽器或 Node 環境執行

  • 包含哪些: :
    1. Web APIs
    2. I/O ( 讀寫、存取 )
    3. 載入 JS 檔案並且執行時,例如  <script>
    4. 渲染畫面 ( Render )
    5. 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
  1. 執行主腳本作為宏任務:
  • 首先,整個 <script> 標籤中的內容被視為一個宏任務</script>
  • 在這個宏任務中,JavaScript 引擎會按順序執行腳本中的同步程式碼
  1. 執行同步任務:
  • 腳本中的同步程式碼(例如 console.log)會被立即執行
  1. 安排微任務和宏任務:
  • 在這個宏任務(即主腳本)的過程中,會遇到非同步操作(如 Promise.thensetTimeout
  • 這些非同步操作不會立即執行,而是會安排相應的微任務(Promise.then)和宏任務(setTimeout
  1. 完成宏任務,處理微任務佇列:
  • 主腳本(初始宏任務)執行完畢後,JavaScript 引擎會處理所有排隊的微任務
  • 這時,Promise.then callback 會被執行。
  1. 執行下一個宏任務:
  • 微任務佇列清空後,事件迴圈會處理下一個宏任務
  • setTimeout callback(如果其延遲時間已到)將被執行。

參考資料

# JavaScript