Event Loop and MacroTasks/MicroTasks in JavaScript

Event Loop and MacroTasks/MicroTasks in JavaScript

Description
The Event Loop is the core mechanism enabling asynchronous programming in JavaScript, responsible for coordinating work between the Call Stack, Web APIs, and Task Queues. MacroTasks and MicroTasks are two types of tasks in the task queue, and their execution priority and timing directly affect the order of asynchronous code. Understanding the difference between them helps avoid pitfalls related to execution order in real-world development.

Solution Process

  1. Basic Concepts

    • Call Stack: The place where synchronous code executes in order. Functions are pushed onto the stack when called and popped off after execution.
    • Web APIs: Asynchronous functionalities provided by the browser (e.g., setTimeout, AJAX), handled by other threads. Upon completion, their callback functions are pushed into the task queue.
    • Event Loop: Continuously checks if the call stack is empty. If it is, it takes the first task from the task queue and pushes it onto the call stack for execution.
  2. Definition of MacroTasks and MicroTasks

    • MacroTasks: Include the entire script, setTimeout, setInterval, I/O operations, UI rendering, etc.
    • MicroTasks: Include Promise.then/catch/finally, MutationObserver, queueMicrotask, etc.
    • Key Rules:
      • After executing one MacroTask, the Event Loop will clear all tasks in the current MicroTask queue.
      • MicroTasks have higher priority than MacroTasks.
  3. Execution Flow Example Analysis
    Step-by-step demonstration of execution order with code:

    console.log("1"); // Synchronous code, outputs immediately
    
    setTimeout(() => {
      console.log("2"); // MacroTask, enters MacroTask queue
    }, 0);
    
    Promise.resolve().then(() => {
      console.log("3"); // MicroTask, enters MicroTask queue
    });
    
    console.log("4"); // Synchronous code, outputs immediately
    

    Step Breakdown:

    • Synchronous Phase:
      1. Output 1console.log("1") executes and clears from the call stack.
      2. Encounter setTimeout, hand its callback to Web API for timing (to be placed in MacroTask queue after 0ms).
      3. Encounter Promise.resolve().then, place its callback in the MicroTask queue.
      4. Output 4 → call stack is cleared.
    • MicroTask Phase:
      Once the call stack is empty, the Event Loop first checks the MicroTask queue, finds the Promise callback, and outputs 3.
    • MacroTask Phase:
      After clearing the MicroTask queue, it takes the setTimeout callback from the MacroTask queue and outputs 2.
      Final Output Order: 1 → 4 → 3 → 2.
  4. Complex Case with Nested Scenarios

    setTimeout(() => console.log("MacroTask1"), 0);
    
    Promise.resolve().then(() => {
      console.log("MicroTask1");
      Promise.resolve().then(() => console.log("MicroTask2"));
    });
    
    queueMicrotask(() => console.log("MicroTask3"));
    

    Execution Order:

    • Synchronous code execution: Register MacroTask1 to its queue, register Promise.then (MicroTask1) to the MicroTask queue, register queueMicrotask (MicroTask3) to the MicroTask queue.
    • Clear MicroTask queue: Execute MicroTask1 in registration order → after output, the nested Promise.then (MicroTask2) is added to the current MicroTask queue, continue clearing the MicroTask queue (MicroTask3 → MicroTask2).
    • Finally execute MacroTask1.
      Output Order: MicroTask1 → MicroTask3 → MicroTask2 → MacroTask1.
  5. Summary and Key Points

    • The MicroTask queue is cleared after each MacroTask execution, and any new MicroTasks generated during this process are added to and executed from the current queue (recursive clearing).
    • MacroTasks execute in rounds of the Event Loop. For example, the delay in setTimeout is the minimum time relative to the completion of the current MacroTask.
    • Common Misconception: setTimeout(fn, 0) does not execute immediately; it waits for the current synchronous code and the MicroTask queue to be cleared before execution.