Event Loop Mechanism in JavaScript

Event Loop Mechanism in JavaScript

Description
The event loop is the core mechanism that enables asynchronous programming in JavaScript. As a single-threaded language, JavaScript uses the event loop model to handle asynchronous operations (such as timers and network requests) without blocking the main thread. Understanding the event loop requires grasping concepts like the call stack, task queue, and microtask queue.

Problem-Solving Process

  1. Basic Concepts

    • Call Stack: Tracks the currently executing function. When a function is called, it is pushed to the top of the stack; after execution completes, it is popped off the stack.
    • Web APIs: Asynchronous features provided by the browser (e.g., setTimeout, fetch). JavaScript delegates these operations to the browser's background processing, and once completed, the callback functions are placed into the task queue.
    • Task Queue (Macrotask Queue): Holds callback functions from asynchronous operations (e.g., callbacks from setTimeout, DOM events).
    • Microtask Queue: Holds callbacks that need to be executed as soon as possible (e.g., Promise.then, MutationObserver).
  2. Workflow of the Event Loop

    • Step 1: Execute synchronous code in the call stack (highest priority). When asynchronous operations are encountered, delegate them to Web APIs for processing.
    • Step 2: After synchronous code execution completes, the event loop first checks the microtask queue, executing all microtasks in sequence (until the queue is empty).
    • Step 3: After microtasks are executed, render the page if necessary (timing determined by the browser).
    • Step 4: Retrieve one macrotask from the task queue and execute it (e.g., a setTimeout callback), then return to Step 2 to check the microtask queue.
  3. Example Analysis

    console.log("Start");
    
    setTimeout(() => console.log("Timeout"), 0);
    
    Promise.resolve().then(() => console.log("Promise"));
    
    console.log("End");
    
    • Execution Process:
      1. Synchronous code: Output "Start" → Delegate setTimeout callback to Web APIs (placed into the task queue after 0 ms) → Place Promise.then callback into the microtask queue → Output "End".
      2. After synchronous code ends, check the microtask queue: Output "Promise".
      3. After the microtask queue is cleared, retrieve the setTimeout callback from the task queue and execute it: Output "Timeout".
  4. Key Rules

    • Microtasks have higher priority than macrotasks (e.g., Promise.then executes before setTimeout).
    • Tasks of the same type are executed in order, but the microtask queue must be completely cleared before processing macrotasks.
  5. Complex Scenario

    setTimeout(() => console.log("Timeout 1"), 0);
    
    Promise.resolve().then(() => {
      console.log("Promise 1");
      setTimeout(() => console.log("Timeout 2"), 0);
    });
    
    Promise.resolve().then(() => console.log("Promise 2"));
    
    • Output Order:
      • After synchronous code execution, the microtask queue contains two Promise.then callbacks.
      • Output "Promise 1" → "Promise 2" in sequence (microtask queue cleared).
      • Execute macrotasks: First output "Timeout 1", then execute the second setTimeout (registered in the microtask) and output "Timeout 2".
  6. Common Pitfalls

    • Recursively adding microtasks within a microtask can block the main thread (as the event loop will continuously clear the microtask queue).
    • The delay time for macrotasks (e.g., setTimeout) is the minimum waiting time and does not guarantee precise execution.

By understanding these steps, you can predict the execution order of asynchronous code and avoid common concurrency issues.