Master JavaScript Event Loop: From Basics to Advanced Examples
This article explains the JavaScript event loop in depth, covering execution contexts, call stacks, macro‑ and micro‑tasks, and how APIs like setTimeout, Promise, process.nextTick, and setImmediate interact, using clear diagrams and step‑by‑step examples for both browsers and Node.js.
Learning JavaScript can feel scattered, so the author seeks a core thread to track progress, focusing on the event loop as the most critical concept for front‑end development.
The event loop determines the execution order of JavaScript code, but many domestic articles only skim the surface, making it hard to grasp, especially after ES6 introduced Promise, which makes understanding the loop even more important.
Before diving in, you should already understand execution context, call stack, queue data structure, and Promise.
Because Chrome’s new standard event loop is similar to Node.js, this article also integrates Node.js concepts, mentioning APIs like process.nextTick and setImmediate that exist in Node but not in browsers.
JavaScript runs on a single thread with a single event loop.
Web workers introduce multithreading, but they are not covered here.
Besides the call stack, JavaScript uses a task queue to manage certain code execution.
There is only one event loop, but multiple task queues can exist.
Task queues are divided into macro‑tasks (now called tasks) and micro‑tasks (now called jobs).
Macro‑tasks include script, setTimeout, setInterval, setImmediate, I/O, UI rendering.
Micro‑tasks include process.nextTick, Promise, Object.observe (deprecated), MutationObserver.
setTimeout/Promise are task sources; the tasks they generate enter the appropriate queues.
Different task sources feed into different queues; setTimeout and setInterval share the same queue.
The event loop starts with the script macro‑task, then empties the call stack, processes all micro‑tasks, and repeats by picking the next macro‑task.
Each task, whether macro or micro, is executed via the call stack.
To illustrate, two examples are provided.
Step 1: The event loop starts with the script macro‑task; only the script task is present.
Step 2: The script encounters setTimeout, a macro‑task source, which dispatches a task to its queue.
Step 3: The script encounters a Promise. The constructor runs immediately, while the .then callback is queued as a micro‑task.
promise1 is pushed onto the stack and logged first.
resolve runs inside the loop, then promise2 logs, followed by then1 entering the micro‑task queue.
After the script finishes, only a global1 log remains.
Step 4: After the macro‑task completes, all executable micro‑tasks run; here only then1 executes.
Step 5: Once micro‑tasks are done, the next loop round starts with macro‑tasks again.
The setTimeout queue still holds timeout1, which runs next.
When both macro‑ and micro‑task queues are empty, execution stops.
A more complex example demonstrates additional interactions with setImmediate, process.nextTick, and multiple setTimeouts.
Step 1: script macro‑task runs, global1 logs.
Step 2: setTimeout dispatches a macro‑task.
Step 3: setImmediate dispatches a macro‑task that runs after setTimeout.
Step 4: process.nextTick queues a micro‑task, which runs before Promise micro‑tasks.
Step 5: Promise constructor runs immediately; its .then is queued as a micro‑task.
Subsequent steps show additional setTimeout, nextTick, Promise, and setImmediate interactions, illustrating the order in which macro‑ and micro‑tasks are processed across multiple loop rounds.
Note that the exact execution order can vary across environments (different Node versions, browsers, etc.).
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.