BRUCE_FEBRUCE_FE

EN/CH Mode

BRUCE_FE JS Interview Notes - Event Loop and Asynchronous Operations

Complete analysis of JavaScript and Node.js event loop, including microtask/macrotask differences, browser and Node.js execution order, nextTick and Promise priorities, and common interview pitfalls and solutions.

影片縮圖

Lazy to read articles? Then watch videos!

What are Asynchronous Operations?

JavaScript is a single-threaded language that cannot handle multiple tasks simultaneously. To prevent blocking the entire program during waiting operations (like API requests or timers), JavaScript introduced asynchronous mechanisms. These async tasks are handled by the browser or Node.js at a lower level, then queued back through the event loop for execution when completed.

  • setTimeout: macrotask, executes after delay
  • Promise.then: microtask, executes immediately after synchronous code
  • async/await: syntactic sugar for Promises
  • queueMicrotask: manually add to microtask queue

Browser Event Loop

In the browser, each round of the event loop follows this execution order:

  1. 1.
    Execute synchronous code (Call Stack)
  2. 2.
    Clear all Microtask queue
  3. 3.
    Execute one Macrotask
  4. 4.
    Repeat the cycle

Microtasks are all executed before the end of each event loop cycle, taking priority over any macrotask.

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

console.log('4');

輸出順序:1 → 4 → 3 → 2

requestAnimationFrame Position in Event Loop

requestAnimationFrame (rAF) is a browser API used for optimizing animation performance. It has a special position in the event loop:

Browser frame processing order:
1. Execute synchronous code
2. Execute all microtasks (Promise.then, queueMicrotask, etc.)
3. Execute requestAnimationFrame callbacks
4. Browser rendering (layout, paint, composite)
5. Execute a macrotask (setTimeout, setInterval, etc.)
6. Return to step 1 to start the next cycle

Node.js Event Loop

Node.js event loop is based on libuv, with six phases, each having its own corresponding tasks. Between each phase, all microtasks (including process.nextTick and Promise) are executed first.

  1. 1.
    timers: handle setTimeout / setInterval
  2. 2.
    pending callbacks: callbacks for some async operations
  3. 3.
    idle, prepare: internal use
  4. 4.
    poll: wait for new I/O events
  5. 5.
    check: execute setImmediate
  6. 6.
    close callbacks: such as socket.on('close')
process.nextTick(() => console.log('tick'));
Promise.resolve().then(() => console.log('promise'));
setTimeout(() => console.log('timeout'), 0);
console.log('sync');

輸出順序:sync → tick → promise → timeout

Microtask vs Macrotask Summary

  • 🟢
    Microtask (executes immediately after sync):Promise.then, queueMicrotask, MutationObserver, async/await
  • 🟡
    Macrotask (next event loop cycle):setTimeout, setInterval, setImmediate
  • 🔴
    Special: In Node.js, process.nextTick has priority over all microtasks

setTimeout vs setImmediate

In Node.js, if setTimeout and setImmediate appear in an I/O callback, setImmediate has priority.

But if executed directly in the main program, the order is unstable:
// 在 I/O callback 中,順序固定
const fs = require('fs');
fs.readFile(__filename, () => {
  setTimeout(() => console.log('timeout'));
  setImmediate(() => console.log('immediate'));
});
// 輸出:immediate → timeout

// 直接執行,順序不固定
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 輸出:順序不固定!
Reason: The setTimeout(fn, 0) in the main program is actually set to 1ms, and the slight difference in the timing of the event loop startup will affect the execution order.

🔥 Common Interview Questions

1. What is Event Loop?

解答:

  1. Microtasks and Macrotasks:

    JavaScript is single-threaded, doing one thing at a time. To handle asynchronous operations, JavaScript divides tasks into two categories:

    • Microtasks: Such as Promise.then, queueMicrotask, MutationObserver
    • Macrotasks: Such as setTimeout, setInterval, MessageChannel, and setImmediate in Node.js

    Execution order: 'Synchronous code → All microtasks → One macrotask → Repeat'

  2. Differences between Node.js and browsers:

    In browsers, each event loop cycle executes all synchronous code and microtasks first, then takes one task from the macrotask queue.

    In Node.js, the event loop has six phases: timers pending callbacks poll check close callbacks. After each phase, Node.js runs process.nextTick and Promise microtasks.

    • process.nextTick: Node.js specific, highest priority
    • Promise.then: A microtask, prioritized over macrotasks
    • setImmediate: Only exists in Node.js, executes in the check phase

2. Event Loop Order

console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');

Answer: Output A → D → C → B

  • 1. First execute synchronous code, printing A and D
  • 2. setTimeout is a macrotask, placed in the next event loop cycle
  • 3. Promise.then is a microtask, executed immediately after synchronous code, printing C
  • 4. Finally, execute the setTimeout callback, printing B

3. Asynchronous Behavior of setTimeout vs setImmediate

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));

Answer: Output order is not fixed (could be timeout → immediate, or immediate → timeout)

  • Explanation: Since this code isn't wrapped in an I/O callback, the execution order cannot be guaranteed and depends on the execution environment and event loop scheduling timing.
  • setTimeout in the timers phase
  • setImmediate in the check phase
  • Since this code isn't wrapped in an I/O callback, the execution order cannot be guaranteed and depends on the execution environment and event loop scheduling timing.

4. nextTick vs Promise

process.nextTick(() => console.log('tick'));
Promise.resolve().then(() => console.log('promise'));
console.log('sync');

Answer: Output sync → tick → promise

  • 1. Synchronous code executes first, printing sync
  • 2. process.nextTick has priority over all microtasks, printing tick
  • 3. Promise.then is a microtask, executed last and printing promise

5. Promise in setTimeout

setTimeout(() => {
  console.log('1');
  Promise.resolve().then(() => console.log('2'));
}, 0);

Promise.resolve().then(() => console.log('3'));
console.log('4');

Answer: Output 4 → 3 → 1 → 2

  • 1. First execute synchronous code, printing 4
  • 2. Promise.then is a microtask, immediately executed and printing 3
  • 3. setTimeout is a macrotask, executed in the next event loop cycle, printing 1
  • 4. Promise.then inside setTimeout is executed immediately after setTimeout completes, printing 2

6. async/await and Event Loop

async function foo() {
  console.log('A');
  await Promise.resolve();
  console.log('B');
}

console.log('C');
foo();
console.log('D');

Answer: Output C → A → D → B

Analysis: async/await breaks at the await point, placing subsequent logic as a microtask in the queue.

7. Mixed Execution Order of setTimeout + Promise + async/await

console.log('start');

setTimeout(() => {
  console.log('timeout');
}, 0);

Promise.resolve().then(() => {
  console.log('promise');
});

(async () => {
  console.log('async1 start');
  await Promise.resolve();
  console.log('async1 end');
})();

console.log('end');

Answer: Output start → async1 start → end → promise → async1 end → timeout

Analysis:

  • Synchronous code executes first: start, async1 start, end
  • Then enter the microtask queue, first executing promise, then async1 end
  • Finally, the macrotask setTimeout

8. What is the Relationship Between requestAnimationFrame and Event Loop?

A: requestAnimationFrame has a special execution timing in the event loop. It is called after all microtasks have finished executing, before rendering:

Event loop frame execution order:
1. Execute synchronous tasks
2. Execute microtask queue (Promise.then, queueMicrotask, etc.)
3. Execute requestAnimationFrame callbacks ← Special timing point
4. Browser rendering (layout, paint, composite)
5. Execute macrotasks (setTimeout, setInterval, etc.)
6. Enter next frame cycle

This allows rAF to update the screen at the last moment before rendering,
ensuring animation calculations are immediately rendered without delay to the next frame.
console.log('script start');

setTimeout(() => {
  console.log('setTimeout 1');

  Promise.resolve().then(() => {
    console.log('microtask inside setTimeout 1');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('microtask 1');
});

Promise.resolve().then(() => {
  console.log('microtask 2');
});

setTimeout(() => {
  console.log('setTimeout 2');
}, 0);

requestAnimationFrame(() => {
  console.log('rAF callback');
});

console.log('script end');

Output order analysis:

  1. First execute synchronous tasks: script start script end
  2. Then execute all microtasks in the current frame: microtask 1 microtask 2
  3. Then execute the requestAnimationFrame callback: rAF callback (before the next repaint)
  4. Browser performs rendering
  5. Finally execute macrotasks: setTimeout 1 microtask inside setTimeout 1 (note this microtask is inside the setTimeout callback, so it executes immediately) → setTimeout 2

Complete output order: script start script end microtask 1 microtask 2 rAF callback setTimeout 1 microtask inside setTimeout 1 setTimeout 2