Mastering JavaScript’s Event Loop: Stack, Queues, and Asynchronous Flow
This article explains JavaScript’s single‑threaded event loop, detailing how the call stack, macro‑ and micro‑task queues, and asynchronous APIs cooperate to schedule code execution, illustrated with diagrams and a step‑by‑step example.
Event Loop Mechanism
JavaScript’s event loop is the execution mechanism that manages the call stack, handling function entry and exit in a single‑threaded environment. Because the language runs on one thread, the event loop coordinates synchronous code, asynchronous APIs, and task queues to prevent race conditions.
Typical flow (illustrated below):
The JavaScript engine parses and executes the script code line by line, building the execution stack.
When asynchronous code is encountered, it is handed off to the appropriate Web API (e.g., timers, network).
Once an asynchronous operation produces a result, its callback is pushed into a task queue, awaiting execution. If no callback is generated, nothing is queued.
After the current stack task finishes, the engine checks the task queue; if it is not empty, one task is dequeued and pushed onto the main thread for execution.
Steps 4 repeats until the queue is empty, constituting one event‑loop iteration. New asynchronous tasks that arise during execution are handled in the same way.
Relevant asynchronous processes:
DOM events – handled by the browser’s DOM module, which adds callbacks to the queue when triggered.
Timers such as setTimeout – managed by the Timer module, which enqueues callbacks after the specified delay.
Ajax/network requests – processed by the Network module, which enqueues callbacks once the response returns.
Stack
Stack (also called a LIFO list) is a linear data structure that permits insertion and removal only at one end.
After understanding that a stack follows a last‑in‑first‑out order, the following diagrams illustrate push and pop operations.
Task Queue
JavaScript maintains multiple task queues with different priorities. Higher‑priority queues are drained first, and within a queue tasks are processed in FIFO order. There are two main types:
macrotask queue – includes script , setTimeout , setInterval , I/O, UI rendering, etc.
microtask queue – includes process.nextTick , native Promise callbacks, MutationObserver , etc.
Differences:
macrotask queue – can have multiple queues with priority levels.
microtask queue – a single queue executed completely before the next macrotask.
The order of the event loop determines JavaScript execution order: start with the script (first macrotask), then run all microtasks, then pick the next macrotask, and repeat. Each iteration processes one macrotask, then drains the microtask queue before moving on.
Example execution flow (illustrated below):
Encounter console.log – executes immediately, outputting “-----start-----”.
setTimeout is handed to the timer module; its callback is later queued as a macrotask.
Encounter a Promise – its then callback is placed in the microtask queue.
Another Promise adds a second microtask.
A second setTimeout is queued as a macrotask; because its delay is shorter, its callback runs before the first timeout.
Another console.log runs immediately, outputting “-----end-----”.
Execution finishes.
MaoDou Frontend Team
Open-source, innovative, collaborative, win‑win – sharing frontend tech and shaping its future.
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.