關於JS背後的運行原理筆記
紀錄關於JS運行原理,Event Loop以及Execute context
我們都知道JS是一個單執行緒、同步的語言,而這表示一次只能執行一個任務。當我們要進行像是非同步(asynchronous)操作的話,我們的JS要透過事件循環(Event Loop)機制,來實現非阻塞(non-blocking)的行為。
並且Event loop的實現,是透過宿主環境所實現的(ex. 瀏覽器、node...),而不是JS引擎實現的。
JS引擎的工作是將高階的 JavaScript 語言轉換為電腦可執行的機器碼,主要是在做解析、編譯、執行和記憶體管理分配等等。
像是nodejs的環境下,使用了 libuv 來實現Event loop機制。而在瀏覽器中,通常是由其內建的渲染引擎(如 Blink)與 Web APIs 整合而成。
一、瀏覽器裡的Event Loop
當瀏覽器執行一個js任務就會做一次Event loop,每次loop結束後會檢查是否需要渲染、處理worker等等的消息,這個任務我們稱為macro task。
但這種任務隊列方式會有個問題,如果任務一直進來,忽然有一個緊急任務,照該方法就會被拖延很久,所以這裡有個特殊設計,專門提供給這種緊急任務的快速通道,而這就是micro tasks,所以每一次loop都會跑:
執行macro task->執行micro tasks->check渲染->check worker
構成javascript的runtime環境再Browser分為三大區塊: Javascript Engine (V8)、Callback Queue以及Web APIs
其中JS引擎有兩個主要部分: Call Stack以及Heap。
並且Event Loop會做下面的事並且重複循環:
- 持續監看 Call Stack 和 Callback Queue 的狀態。
- 當 Call Stack 清空了(表示當前程式執行已沒有同步程式碼在執行)
- Event Loop 才會將 Callback Queue 中第一個排隊的回呼函式推入 Call Stack 中執行
1. JS引擎
用來負責解譯並執行 JavaScript 程式碼與記憶體配置,像是V8、JavascriptCore。
Stack 負責程式呼叫流程的有序管理(哪個函式正在執行,函式內的區域變數對應的 Heap 物件引用等),而 Heap 則是實際用來儲存物件實體記憶體資料的空間。
Call Stack (注意,不是callback,是call)
用來記錄程式執行「位置」的堆疊結構。當你呼叫一個函式,對應的執行上下文會被push到 Call Stack;當函式執行完畢,會將其pop出 Stack。
只有當 Call Stack 為空,JS引擎才有空閒執行其他任務(例如去處理已準備就緒的回呼)。
Heap
用來儲存物件、陣列等較複雜的資料結構的記憶體配置區域。JavaScript 中的物件是存在 Heap 中,而原始型別 (Primitive type) 的值則通常存在於較容易分配的記憶區中。
當code產生新物件,JS引擎會在 Heap 中為該物件分配空間。
2. Callback Queue (Task Queue)
任務(回呼函式)在此等待被執行。這些回呼函式不會直接在 Web APIs 執行完後馬上跑進 JS引擎的Stack裡,而是先進入 Callback Queue。
只有在 Call Stack 為空、Event Loop 看到有任務在 Callback Queue 等候時,才會將該任務取出並執行。
Steps:
-
異步任務 (例如
setTimeout
的回呼函式、fetch
完成後的回傳回呼、事件監聽器所觸發的回呼等) 在 Web API 層面執行完畢, -
瀏覽器會將對應的回呼函式(callback function)放入 Callback Queue 中排隊等待。
3. Web APIs
瀏覽器環境中提供了一系列的 API 供 JavaScript 使用,這些 API 並非由 JavaScript Engine 所提供,而是由瀏覽器本身實作並掛載到 JavaScript 執行環境之中
Steps:
-
JavaScript 程式呼叫到這些 Web API 時,實際執行任務的並不是 JavaScript Engine 本身,而是交由瀏覽器背後所提供的多執行緒機制(如異步 I/O、計時器、網路請求等)來處理。
-
完成後,瀏覽器會將對應的 callback 放入 "Callback Queue" 等待被 JavaScript Engine 處理。
ex.
- DOM API:可操作網頁文件物件模型(如
document.querySelector()
、document.createElement()
)。 - 定時器 API:
setTimeout()
、setInterval()
等,用來設定延遲或週期性任務。 - 網路請求 API:如
fetch()
、XMLHttpRequest()
,用於發出 HTTP 請求。 - 其他瀏覽器提供的功能:如 Geolocation、WebSockets、Canvas、Web Audio API 等。
瀏覽器 Event Loop 架構
┌─────────────────────────────────────────────────────────────────────┐
│ 瀏覽器環境 (Browser Environment) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ JavaScript Engine │ │ Web APIs │ │
│ │ (V8) │ │ │ │
│ │ │ │ • setTimeout() │ │
│ │ ┌─────────────┐ │ │ • fetch() │ │
│ │ │ Call Stack │ │ │ • DOM APIs │ │
│ │ │ │ │ │ • Event Listeners │ │
│ │ │ [Function3] │◄───┼────┤ • XMLHttpRequest │ │
│ │ │ [Function2] │ │ │ │ │
│ │ │ [Function1] │ │ │ │ │
│ │ │ main() │ │ │ │ │
│ │ └─────────────┘ │ └─────────────────────┘ │
│ │ │ │ │
│ │ ┌─────────────┐ │ │ Callback 完成 │
│ │ │ Heap │ │ ▼ │
│ │ │ │ │ ┌─────────────────────┐ │
│ │ │ [Objects] │ │ │ Macro Task Queue │ │
│ │ │ [Arrays] │ │ │ │ │
│ │ │ [Variables] │ │ │ [setTimeout] │ │
│ │ └─────────────┘ │ │ [setInterval] │ │
│ └─────────────────────┘ │ [UI Events] │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ ▲ │
│ │ Micro Task Queue │ │ │
│ │ │ │ │
│ │ [Promise.then] │ │ │
│ │ [queueMicrotask] │ │ │
│ │ [MutationObserver] │ │ │
│ └─────────────────────┘ │ │
│ ▲ │ │
└───────────┼──────────────────────────┼─────────────────────────────┘
│ │
└──────────────────────────┼─────────────────────┐
│ │
┌─────────────────────┼─────────────────────┴─────┐
│ Event Loop │
│ │
│ 1. 檢查 Call Stack 是否為空 │
│ 2. 優先處理所有 Micro Tasks │
│ 3. 處理渲染相關任務 │
│ 4. 處理一個 Macro Task │
└─────────────────────────────────────────────────┘
Event Loop 執行週期:
在瀏覽器中:
在瀏覽器中,Event Loop每一輪的執行順序為:
1. 執行"同步程式碼"立即執行 (Call Stack)
2. 執行所有 microtasks (Promise.then, queueMicrotask 等)
3. 執行 requestAnimationFrame 回調
4. 瀏覽器渲染 (layout, paint, composite)
5. 執行一個 macrotask (setTimeout, setInterval 等)
6. 返回步驟 1 開始下一輪循環
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 執行一個 │───▶│ 執行所有 │───▶│ 檢查 │───▶│ 檢查 │
│ Macro Task │ │ Micro Tasks │ │ 渲染 │ │ Worker │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
▲ │
│ │
└────────────────────────────────────────────────────────┘
重複循環
執行流程說明:
步驟 1: JavaScript 呼叫 Web API
┌─────────────┐ API 呼叫 ┌─────────────┐
│ Call Stack │─────────────▶│ Web APIs │
│ │ │ │
│setTimeout() │ │ 計時器開始 │
└─────────────┘ └─────────────┘
步驟 2: Web API 完成後,Callback 進入 Queue
┌─────────────┐ ┌─────────────┐
│ Web APIs │ 完成後放入 │ Callback │
│ │─────────────▶│ Queue │
│ 計時器結束 │ │ [callback] │
└─────────────┘ └─────────────┘
步驟 3: Event Loop 檢查並移動任務
┌─────────────┐ ┌─────────────┐
│ Call Stack │ Stack 空時 │ Callback │
│ │◄─────────────│ Queue │
│ (空) │ 移動任務 │ [callback] │
└─────────────┘ └─────────────┘
特性: • Call Stack 必須完全清空,Event Loop 才會移動 Callback Queue 中的任務 • Micro Tasks 優先級高於 Macro Tasks • 一次 Event Loop 只處理一個 Macro Task,但會處理所有 Micro Tasks • Web APIs 在背景執行,不阻塞主線程
二、Nodejs裡的Event Loop
在Nodejs中
Node.js
的 Event Loop 機制是其非阻塞 I/O 和高併發能力的核心。它使得 Node.js
可以使用單一線程來處理數以千計的並行連線。雖然 JavaScript
本身是單線程的,但 Node.js
在底層透過 libuv
來處理各種異步操作(如文件讀寫、網絡請求等),而 Event Loop 就是協調這一切的總指揮。
在Node中,Event Loop每一輪的執行順序為:
基於libuv,每一階段之間會先執行所有 microtask(含 process.nextTick 和 Promise)。
1. process.nextTick() 隊列:清空此隊列。
2. Microtask (Promise) 隊列:清空此隊列。
3. 進入 Event Loop 的六個階段之一 (例如 timers 階段)。
- 執行該階段的 Macrotask (宏任務) 隊列。
- 執行完一個宏任務後,立即回頭檢查並清空 process.nextTick() 和 Microtask 隊列。 (這點非常重要!)
- 重複上述步驟,直到該階段的隊列清空。
1. 切換到 Event Loop 的下一個階段。
- 在切換之前,再次清空 process.nextTick() 和 Microtask 隊列。
1. 重複步驟 3 和 4,直到所有階段都走過一遍。
2. 進入下一個 tick,循環往復。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ <-- I/O polling
│ ┌─────────────┴─────────────┐
│ │ poll │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
三、關於執行上下文 (Execution Context) 與呼叫堆疊 (Call Stack)
什麼是執行上下文?
執行上下文(Execution Context)是 JS 程式碼在被評估(evaluate)與執行(execute)時所處的環境,只在特定的區塊內有效,一旦超出該區塊便無法使用。並且所有 JavaScript 代碼的運行皆發生於某個執行上下文之中。
並且該環境基本上包含了:
- 變數和函式的儲存空間
this
的指向- 作用域鏈 (Scope Chain)
執行上下文的種類
1. 全域執行上下文 (Global Execution Context, GEC)
- 唯一性:整個程式只有一個
- 建立時機:程式開始執行時自動建立
- 作用:作為所有程式碼的基礎環境
- 全域物件:
- 瀏覽器環境:
window
- Node.js 環境:
global
- 通用標準:
globalThis
(ES2020+)
- 瀏覽器環境:
2. 函式執行上下文 (Function Execution Context, FEC)
- 建立時機:每次呼叫函式時建立
- 數量:可以有很多個,每個函式呼叫都會建立一個
- 生命週期:函式執行完畢後就會被銷毀
3. Eval 執行上下文
- 建立時機:使用
eval()
函式時建立
呼叫堆疊 (Call Stack) 的運作原理
執行流程
- 程式開始:GEC 被建立並放入堆疊底部
- 呼叫函式:FEC 被建立並推入堆疊頂端
- 函式完成:FEC 從堆疊頂端移除
- 回到上層:控制權回到下一個執行上下文
- 程式結束:最終只剩下 GEC
Example
// 範例程式碼
function outer() {
console.log('進入 outer func');
inner();
console.log('離開 outer func');
}
function inner() {
console.log('在 inner func內');
}
console.log('程式開始');
outer();
console.log('程式結束');
執行順序與堆疊變化:
1. [GEC] - "程式開始"
2. [GEC, outer] - "進入 outer 函式"
3. [GEC, outer, inner] - "在 inner 函式內"
4. [GEC, outer] - inner 執行完畢
5. [GEC] - "離開 outer 函式","程式結束"
記憶體管理:
記憶體分工:Stack 管理執行流程,Heap 儲存物件資料
堆疊 (Stack)
- 儲存內容:執行上下文、原始型別的值、物件的參考
- 特性:速度快、大小有限、自動管理
堆積 (Heap)
- 儲存內容:物件、陣列、函式等複雜資料結構
- 特性:大小彈性、需要垃圾回收機制
// 記憶體分配範例
let number = 42; // 原始型別,可能存在 Stack
let person = { // 物件存在 Heap
name: "Alice",
age: 30
};
// Stack 中的 person 變數存的是指向 Heap 中物件的參考
關於全域物件
全域物件是全域執行上下文的核心,它:
- 提供內建的全域函式和變數
- 作為全域變數的容器
- 在不同環境中有不同的名稱
// 不同環境下的全域物件
console.log(globalThis); // 通用寫法
console.log(window); // 瀏覽器
console.log(global); // Node.js
console.log(self); // Web Worker
重點
- 執行上下文是程式碼執行的環境容器
- 呼叫堆疊管理多個執行上下文的執行順序
- 全域執行上下文是程式的基礎,只有一個
- 函式執行上下文在函式呼叫時建立,執行完畢後銷毀
- 記憶體分工:Stack 管理執行流程,Heap 儲存物件資料