Detailed Explanation of Browser Event Loop Mechanism
1. Problem Description
The browser Event Loop is the core mechanism that enables asynchronous programming in JavaScript. It determines the execution order of code, managing the scheduling between task queues, macro tasks, and micro tasks. Understanding the Event Loop helps developers avoid common asynchronous pitfalls (such as execution order issues) and optimize performance.
2. Key Concepts
-
Single-Threaded and Asynchronous Nature:
- JavaScript is a single-threaded language, but browsers provide Web APIs (e.g., setTimeout, DOM events, Ajax) to handle asynchronous operations.
- Once an asynchronous task completes, its callback function is placed into a task queue, waiting to be invoked by the main thread.
-
Components of the Event Loop:
- Call Stack: Executes synchronous code (Last In, First Out).
- Task Queue: Includes the Macro Task Queue and the Micro Task Queue.
- Event Loop Thread: Continuously checks if the call stack is empty, then fetches and executes tasks from the queues according to rules.
3. Task Classification and Execution Rules
-
Macro Tasks:
- Sources: Entire script code, setTimeout, setInterval, I/O operations, UI rendering, DOM event callbacks.
- Characteristic: Only one macro task is executed per Event Loop cycle.
-
Micro Tasks:
- Sources: Promise.then/catch/finally, async/await, MutationObserver, queueMicrotask.
- Characteristic: All micro tasks are executed immediately after the current macro task finishes, until the micro task queue is cleared.
4. Event Loop Process (Step-by-Step Details)
-
Execute Global Script (Macro Task):
- Synchronous code is pushed onto the stack and executed line by line. When asynchronous APIs (like setTimeout) are encountered, their callback functions are registered in the Web API environment, waiting to be triggered.
- Example:
console.log("1"); setTimeout(() => console.log("2"), 0); Promise.resolve().then(() => console.log("3")); console.log("4"); - Synchronous code output: 1, 4.
-
Process Micro Tasks:
- After the current macro task (the script) finishes, the micro task queue is checked (which now contains the Promise callback).
- Execute all micro tasks sequentially, output: 3.
-
Render Update (If Needed):
- The browser may perform UI rendering at this stage (some browsers treat rendering as a macro task).
-
Fetch the Next Macro Task:
- Take the setTimeout callback from the macro task queue, execute it, and output: 2.
5. Complex Scenario Analysis
// Scenario: Nested Micro Tasks and Macro Tasks
console.log("Start");
setTimeout(() => {
console.log("Timeout");
Promise.resolve().then(() => console.log("Promise inside Timeout"));
}, 0);
Promise.resolve()
.then(() => {
console.log("Promise 1");
queueMicrotask(() => console.log("Microtask in Promise"));
})
.then(() => console.log("Promise 2"));
console.log("End");
Execution Steps:
- Synchronous code output: Start, End.
- Micro task queue:
- Execute Promise 1, output "Promise 1", and add a new micro task (Microtask in Promise).
- Execute all tasks in the micro task queue (including the newly added one), output "Microtask in Promise", then trigger the next 'then' in the Promise chain, output "Promise 2".
- Macro task queue: Execute the setTimeout callback, output "Timeout". The internal Promise micro task is executed immediately, outputting "Promise inside Timeout".
Final Output Order:
Start → End → Promise 1 → Microtask in Promise → Promise 2 → Timeout → Promise inside Timeout
6. Common Interview Questions
-
Why do micro tasks have higher priority than macro tasks?
- To ensure the timeliness of asynchronous operations (such as Promise state updates) and avoid blocking rendering.
-
Does setTimeout(fn, 0) execute immediately?
- No, it waits at least 4ms (browser specification) and must wait for the current macro task and all micro tasks to complete.
-
Execution order of async/await:
- Code before 'await' executes synchronously; code after 'await' is equivalent to Promise.then (a micro task).
7. Summary
The core principle of the Event Loop is: Synchronous tasks first → Clear all micro tasks → Render → Execute macro tasks sequentially. Mastering this mechanism effectively helps in debugging asynchronous code and optimizing performance.