Lesson 43-Event Loop Source Code Simulation

Event Loop Core Principle Simulation

Task Queue Basic Structure

Let’s start by simulating a simplified version of the Event Loop’s core structure:

class EventLoopSimulator {
  constructor() {
    this.macroTaskQueue = []; // Macrotask queue
    this.microTaskQueue = []; // Microtask queue
    this.currentPhase = 'idle'; // Current phase
    this.timerIdCounter = 1; // Timer ID counter
    this.timers = new Map(); // Timer storage
  }

  // Add macrotask
  addMacroTask(task) {
    this.macroTaskQueue.push(task);
    this.runEventLoop();
  }

  // Add microtask
  addMicroTask(task) {
    this.microTaskQueue.push(task);
  }

  // Simulate setTimeout
  setTimeout(callback, delay) {
    const timerId = this.timerIdCounter++;
    const triggerTime = Date.now() + delay;

    this.timers.set(timerId, {
      callback,
      triggerTime
    });

    // In real implementations, a system timer would be used
    // We simplify by checking timers in runEventLoop
    return timerId;
  }

  // Simulate clearTimeout
  clearTimeout(timerId) {
    this.timers.delete(timerId);
  }

  // Simulate process.nextTick (Node.js specific)
  nextTick(callback) {
    // nextTick queue has higher priority than microtasks
    this.microTaskQueue.unshift(() => {
      callback();
      // nextTick callbacks may add new nextTick callbacks
      // So we need to continue processing the nextTick queue
      this.processNextTickQueue();
    });
  }

  // Process nextTick queue
  processNextTickQueue() {
    while (this.microTaskQueue.length > 0 &&
           this.microTaskQueue[0].isNextTick) {
      const task = this.microTaskQueue.shift();
      task();
    }
  }

  // Run event loop
  runEventLoop() {
    if (this.currentPhase !== 'idle') return;

    this.currentPhase = 'running';

    // 1. Execute one macrotask
    if (this.macroTaskQueue.length > 0) {
      const macroTask = this.macroTaskQueue.shift();
      console.log('Executing macrotask:', macroTask.name || 'anonymous');
      macroTask();
    }

    // 2. Execute all microtasks
    this.processMicroTasks();

    // 3. Check timers (in real implementations, triggered by system timers)
    this.checkTimers();

    // 4. Rendering (browser environment)
    // Simplified here

    // 5. Check if new macrotasks need execution
    if (this.macroTaskQueue.length > 0) {
      // Continue next event loop iteration
      setImmediate(() => this.runEventLoop());
    } else {
      this.currentPhase = 'idle';
    }
  }

  // Process microtask queue
  processMicroTasks() {
    // Process nextTick queue first (if any)
    this.processNextTickQueue();

    // Then process regular microtasks
    while (this.microTaskQueue.length > 0) {
      const microTask = this.microTaskQueue.shift();
      console.log('Executing microtask:', microTask.name || 'anonymous');
      microTask();
    }
  }

  // Check timers
  checkTimers() {
    const now = Date.now();
    const expiredTimers = [];

    // Find all expired timers
    for (const [timerId, timer] of this.timers) {
      if (timer.triggerTime <= now) {
        expiredTimers.push(timerId);
      }
    }

    // Execute expired timer callbacks (as macrotasks)
    for (const timerId of expiredTimers) {
      const timer = this.timers.get(timerId);
      this.addMacroTask(() => {
        console.log('Executing timer callback:', timerId);
        timer.callback();
      });
      this.timers.delete(timerId);
    }
  }
}

Complete Event Loop Simulation Implementation

Below is a more complete Event Loop simulation implementation, including macrotasks, microtasks, and nextTick queue:

class AdvancedEventLoopSimulator {
  constructor() {
    this.macroTaskQueue = []; // Macrotask queue
    this.microTaskQueue = []; // Microtask queue
    this.nextTickQueue = []; // nextTick queue (Node.js specific)
    this.currentPhase = 'idle'; // Current phase
    this.timerIdCounter = 1; // Timer ID counter
    this.timers = new Map(); // Timer storage
    this.animationFrameCallbacks = []; // requestAnimationFrame callbacks
    this.isProcessing = false; // Whether event loop is processing
  }

  // Add macrotask
  addMacroTask(task, name) {
    this.macroTaskQueue.push({
      task,
      name: name || 'anonymous macro task'
    });
    this.runEventLoop();
  }

  // Add microtask
  addMicroTask(task, name) {
    this.microTaskQueue.push({
      task,
      name: name || 'anonymous micro task'
    });
  }

  // Add nextTick task (Node.js specific)
  nextTick(callback, name) {
    this.nextTickQueue.push({
      callback,
      name: name || 'anonymous nextTick task'
    });
    this.runEventLoop();
  }

  // Simulate setTimeout
  setTimeout(callback, delay, name) {
    const timerId = this.timerIdCounter++;
    const triggerTime = Date.now() + delay;

    this.timers.set(timerId, {
      callback: () => {
        this.addMacroTask(() => {
          console.log(`Executing timer callback(${name || timerId}):`);
          callback();
        });
      },
      triggerTime
    });

    // In real implementations, a system timer would be used
    // We simplify by checking timers in runEventLoop
    return timerId;
  }

  // Simulate clearTimeout
  clearTimeout(timerId) {
    this.timers.delete(timerId);
  }

  // Simulate setImmediate (Node.js specific)
  setImmediate(callback, name) {
    this.addMacroTask(() => {
      console.log(`Executing setImmediate callback(${name || 'anonymous'}):`);
      callback();
    }, `setImmediate(${name || 'anonymous'})`);
  }

  // Simulate process.nextTick (Node.js specific)
  processNextTick(callback, name) {
    this.nextTick(callback, name);
  }

  // Simulate Promise.resolve().then() (microtask)
  promiseThen(callback, name) {
    this.addMicroTask(() => {
      console.log(`Executing Promise microtask(${name || 'anonymous'}):`);
      callback();
    }, `promise.then(${name || 'anonymous'})`);
  }

  // Simulate requestAnimationFrame (browser specific)
  requestAnimationFrame(callback, name) {
    this.animationFrameCallbacks.push({
      callback,
      name: name || 'anonymous animation frame'
    });
    // In real implementations, called by browser on next frame
    // We simplify by checking in runEventLoop
    this.addMacroTask(() => {
      if (this.animationFrameCallbacks.length > 0) {
        const callbacks = this.animationFrameCallbacks.slice();
        this.animationFrameCallbacks = [];
        for (const { callback, name } of callbacks) {
          console.log(`Executing requestAnimationFrame(${name || 'anonymous'}):`);
          callback();
        }
      }
    }, 'animation frame processing');
  }

  // Run event loop
  runEventLoop() {
    if (this.isProcessing) return;
    this.isProcessing = true;

    // Use setImmediate or setTimeout to simulate event loop iterations
    // Simplified here with direct recursion
    // Real implementations use system event loop mechanisms
    setTimeout(() => {
      this.processEventLoopPhase();
      this.isProcessing = false;

      // If tasks remain, continue next event loop iteration
      if (this.macroTaskQueue.length > 0 ||
          this.microTaskQueue.length > 0 ||
          this.nextTickQueue.length > 0) {
        this.runEventLoop();
      }
    }, 0);
  }

  // Process one event loop phase
  processEventLoopPhase() {
    console.log('--- Starting event loop phase ---');

    // 1. Process nextTick queue (Node.js specific)
    this.processNextTickQueue();

    // 2. Process microtask queue
    this.processMicroTaskQueue();

    // 3. Process macrotask queue
    this.processMacroTaskQueue();

    // 4. Check timers
    this.checkTimers();

    // 5. Process requestAnimationFrame callbacks (browser specific)
    this.processAnimationFrameCallbacks();

    console.log('--- Ending event loop phase ---\n');
  }

  // Process nextTick queue
  processNextTickQueue() {
    while (this.nextTickQueue.length > 0) {
      const { callback, name } = this.nextTickQueue.shift();
      console.log(`Executing nextTick task(${name || 'anonymous'}):`);
      callback();

      // nextTick callbacks may add new tasks
      // Continue processing until queue is empty
      // Simplified here; Node.js implementation is more complex
    }
  }

  // Process microtask queue
  processMicroTaskQueue() {
    while (this.microTaskQueue.length > 0) {
      const { task, name } = this.microTaskQueue.shift();
      console.log(`Executing microtask(${name || 'anonymous'}):`);
      task();
    }
  }

  // Process macrotask queue
  processMacroTaskQueue() {
    if (this.macroTaskQueue.length > 0) {
      const { task, name } = this.macroTaskQueue.shift();
      console.log(`Executing macrotask(${name || 'anonymous'}):`);
      task();
    }
  }

  // Check timers
  checkTimers() {
    const now = Date.now();
    const expiredTimers = [];

    // Find all expired timers
    for (const [timerId, timer] of this.timers) {
      if (timer.triggerTime <= now) {
        expiredTimers.push(timerId);
      }
    }

    // Execute expired timer callbacks (as macrotasks)
    for (const timerId of expiredTimers) {
      const timer = this.timers.get(timerId);
      timer.callback();
      this.timers.delete(timerId);
    }
  }

  // Process requestAnimationFrame callbacks
  processAnimationFrameCallbacks() {
    if (this.animationFrameCallbacks.length > 0) {
      const callbacks = this.animationFrameCallbacks.slice();
      this.animationFrameCallbacks = [];
      for (const { callback, name } of callbacks) {
        console.log(`Executing requestAnimationFrame(${name || 'anonymous'}):`);
        callback();
      }
    }
  }
}

Task Queues and Execution Flow

Macrotask and Microtask Queues

Macrotasks:

  • script (entire code)
  • setTimeout/setInterval
  • I/O operations
  • UI rendering (browser)
  • setImmediate (Node.js)
  • requestAnimationFrame (browser)

Microtasks:

  • Promise.then/catch/finally
  • process.nextTick (Node.js)
  • MutationObserver (browser)

Event Loop Execution Flow Simulation

Let’s simulate a complete event loop execution flow:

// Create event loop simulator instance
const eventLoop = new AdvancedEventLoopSimulator();

// Simulate a series of tasks
console.log('Script start (macrotask)');

// Add some macrotasks
eventLoop.addMacroTask(() => {
  console.log('Macrotask 1');

  // Add microtask within macrotask
  eventLoop.promiseThen(() => {
    console.log('Microtask 1 (added in macrotask 1)');
  }, 'microtask in macrotask1');

  // Add nextTick within macrotask (Node.js)
  eventLoop.processNextTick(() => {
    console.log('nextTick 1 (added in macrotask 1)');
  }, 'nextTick in macrotask1');
}, 'macrotask1');

eventLoop.addMacroTask(() => {
  console.log('Macrotask 2');

  // Add another macrotask (setTimeout) within macrotask
  eventLoop.setTimeout(() => {
    console.log('setTimeout callback (added in macrotask 2)');

    // Add microtask within setTimeout callback
    eventLoop.promiseThen(() => {
      console.log('Microtask 2 (added in setTimeout callback)');
    }, 'microtask in setTimeout');
  }, 0, 'setTimeout in macrotask2');
}, 'macrotask2');

// Add microtask
eventLoop.promiseThen(() => {
  console.log('Microtask 1 (directly added)');
}, 'direct microtask1');

// Add nextTick (Node.js)
eventLoop.processNextTick(() => {
  console.log('nextTick 1 (directly added)');
}, 'direct nextTick1');

// Add another microtask
eventLoop.promiseThen(() => {
  console.log('Microtask 2 (directly added)');

  // Add macrotask (setImmediate) within microtask
  eventLoop.setImmediate(() => {
    console.log('setImmediate callback (added in microtask 2)');
  }, 'setImmediate in microtask2');
}, 'direct microtask2');

// Add requestAnimationFrame (browser)
eventLoop.requestAnimationFrame(() => {
  console.log('requestAnimationFrame callback');
}, 'animation frame');

console.log('Script end (macrotask)');

Expected Output Analysis

Based on the Event Loop execution order, the expected output for the above code should be:

Script start (macrotask)
Script end (macrotask)
Executing nextTick task(direct nextTick1):
nextTick 1 (directly added)
Executing setImmediate callback(setImmediate in nextTick1):
setImmediate callback (added in nextTick 1)
Executing microtask(direct microtask1):
Microtask 1 (directly added)
Executing microtask(direct microtask2):
Microtask 2 (directly added)
Executing setImmediate callback(setImmediate in microtask2):
setImmediate callback (added in microtask 2)
Executing macrotask(macrotask1):
Macrotask 1
Executing microtask(microtask in macrotask1):
Microtask 1 (added in macrotask 1)
Executing nextTick task(nextTick in macrotask1):
nextTick 1 (added in macrotask 1)
Executing macrotask(macrotask2):
Macrotask 2
Executing macrotask(setTimeout in macrotask2):
setTimeout callback (added in macrotask 2)
Executing microtask(microtask in setTimeout):
Microtask 2 (added in setTimeout callback)
Executing requestAnimationFrame(animation frame):
requestAnimationFrame callback

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Share your love