前端基礎

-

JavaScript 的重要觀念 - Event loop 事件循環詳解

this.web

JS Event Loop 封面圖

甚麼是 Event loop

當我們在瀏覽器中執行 JS 時,通常會遇到一些需要時間來完成的任務,例如向服務器請求數據。這些任務可能需要花費很長時間來完成,但是我們又不能讓 JS 的執行停頓下來,導致頁面無法響應用戶的操作。

因此,JS 引入了一種稱為「事件循環 event loop」的機制,來協調這些任務和 JavaScript 主線程的執行。

但在了解 event loop 之前,我們要先知道一些 JS 的觀念。 整篇文章的架構如下:

  1. JS 是單線程、阻塞 blocking
  2. 執行堆疊 call stack
  3. 瀏覽器是多線程
  4. 主線程、event loop、工作佇列
  5. 簡單的示範

單線程 single threaded

JS 是單線程(single threade runtime)的程式語言,也就是他一次只能一件事情,無法同時處理多個事情

也因為如此,若有一段程式碼需要處理很長的時間,就會造成阻塞。

阻塞 blocking

假設有段程式碼需要很大的運算,例如

let total = 0;
console.log(1);
for (let i = 0; i < 1000000000; i++) {
  total += i;
}
console.log(2);

會發現 console.log(2),需要等待一段時間才會執行,這被稱為阻塞 blocking

阻塞時,瀏覽器沒辦法處理其他事情,這會造成網頁的 lag。

執行堆疊 call stack

前面說 JS 是單線程的,那JS 是怎麼知道程式碼執行的順序呢?

在 JS 中的有一個容器負責記錄主線程中要執行的程式碼,也就是執行堆疊(call stack),假設現在有段程式碼:

console.log(1);
console.log(2);

JS 會執行以下步驟:

  1. 將 console.log(1) 放入堆疊 call stack
  2. 執行 console.log(1)
  3. 將 console.log(2) 放入堆疊 call stack
  4. 執行 console.log(2)

如果執行到某個函式時,便會把這個函數內部要執行的程式碼,添加到堆疊中,若程式遇到 return,則會將整個函數從堆疊的最上方抽離 (pop)。

換句話說,就是遇到 return,函數內部後面的程式碼就不執行了。

不過若函數是無窮迴圈:

function loop() {
  return loop();
}
loop();

那 call stack 將會被不斷疊加上去,直到瀏覽器出現錯誤。

瀏覽器是多線程

雖然 JS 是單線程,不過瀏覽器是多線程的,多線程的瀏覽器能夠同時運行很多事情,也是因為這樣,瀏覽器讓 JS 能夠看起來一次處理很多事情, 像是 DOM、AJAX、setTimeout... 等等都是在瀏覽器上提供的 API,它們都是在瀏覽器上運行。

而瀏覽器運行完後,會將回調函數放到另一個容器 - 工作佇列 callback queue,等到 "執行堆疊 call stack" 裡面的內容全部執行完後,才會執行工作佇列裡的回調函數。

那是誰來判斷 call stack 裡面的任務是否執行完了呢?沒錯,就是事件循環 event loop,他會判斷 call stack 是否為空

若為空,就將 task queue 中的第一個項目放到 call stack ,讓他被執行。

JS 主線程和 event loop

所以簡單說,JS 會利用執行堆疊來紀錄要執行的主線程程式碼,接著將所有需要處理的任務照順序執行。

但是有些特別的任務需要花費較長時間來完成,例如DOM、AJAX、setTimeout...等, JS 主線程就會將這個任務交給瀏覽器處理,同時繼續執行流水線中的其他任務。

當特別的任務完成時,裡面的回調函數會先被放到旁邊 (工作佇列 callback queue),等待 JS 執行完主線程的任務後,才會被放回主線程中執行回調函數,而判斷 JS 是否執行完主線程的東西就是 event loop。

整體會像這樣 👇

Event loop

簡單的 event loop 示範

我們來利用 setTimeout 來簡單模擬 event loop

console.log('1');

setTimeout(() => {
  let total = 0;
  console.log(3);
  for (let i = 0; i < 1000000000; i++) {
    total += i;
  }
  console.log(4);
}, 1000);

console.log('2');

// 1
// 2 (馬上執行)
// 3 (一秒後執行)
// 4 (一秒後過一段時間執行)

在執行這段程式碼時,

  1. 執行 1
  2. 接著將 setTimeout,給瀏覽器處理計時問題。
  3. 執行 2
  4. 一秒後將 setTimeout 回調函數放到 工作佇列(task queue)
  5. 等到所有執行堆疊 callback queue中的內容皆被執行完後,執行回調函數。
  6. 執行 3
  7. 運算 total
  8. 執行 4

這裡的 setTimeout 可以想像成向服務器請求數據。

觀察 JS event loop 的實用工具網站

除了上面的示範,我也分享一個視覺化的網站給你。

我們可以利用 這個網站 loupe 實際看到 js event loop 究竟在搞什麼鬼。

以它提供的例子來說:

$.on('button', 'click', function onClick() {
  setTimeout(function timer() {
    console.log('You clicked the button!');
  }, 2000);
});

console.log('Hi!');

setTimeout(function timeout() {
  console.log('Click the button!');
}, 5000);

console.log('Welcome to loupe.');
  1. $.on(...) 放入 call stack
  2. $.on(...) 放入 webAPIs 等待被觸發
  3. console.log("Hi!") 放入 call stack
  4. setTimeout() 放入 webAPIs,並等待五秒的時間
  5. 同時將 console.log("Welcom to loupe") 放到 call stack
  6. 五秒後將 setTimeout() 中的 callback 放到 callback queue
  7. event loop 將 callback queue 中的 function 放到 call stack 執行

而點擊按鈕後

  1. 將 click 事件放到 callback queue,並等待所有 call stack 事件執行完後才會被放到 call stack 執行,
  2. 再將 setTimeout 放入 webAPIs 計時,計時完後放到 callback queue
  3. 再由 event loop 放到 call stack 執行回調函數

ajax 請求也是同理,所以 js 中看似同時執行很多事情是因為 webAPIs、callback queue、event loop 的幫忙,js 本身還是單線程的。

總結

所以 JS 在做 ajax 請求時能不阻塞程式碼,是因為請求的過程由瀏覽器完成了,完成後會由 event loop 判斷 JS 主線程是否空閒,若為空閒,JS 會處理回調函數裡面的程式碼片段,這就是 event loop 的功能。

那今天就這樣,下篇貼文見~!

相關系列文章