JavaScript is often described as single-threaded, non-blocking, asynchronous, and concurrent. While these terms can seem daunting, the underlying concepts are straightforward when you understand how the Event Loop, Call Stack, Microtask Queue, and Macrotask Queue work together. In this article, we’ll demystify these concepts with explanations and code examples.
The Call Stack
The Call Stack is where JavaScript keeps track of function execution. It operates on the "last in, first out" (LIFO) principle. When a function is called, it’s added to the top of the stack. Once the function finishes execution, it’s removed from the stack.
Example:
function greet() {
console.log("Hello!");
}
function sayGoodbye() {
console.log("Goodbye!");
}
function main() {
greet();
sayGoodbye();
}
main();
Execution Flow:
main()
is added to the stack.greet()
is called and added to the stack.console.log('Hello!')
runs and is removed from the stack.greet()
finishes and is removed.sayGoodbye()
is added to the stack.console.log('Goodbye!')
runs and is removed.sayGoodbye()
finishes and is removed.main()
finishes and is removed.
The Event Loop
The Event Loop ensures that JavaScript operations run smoothly by coordinating tasks between the Call Stack and Task Queues. It monitors the call stack and queues, pushing tasks from the queues into the stack when the stack is empty.
Microtask Queue
The Microtask Queue has the highest priority. It handles tasks like resolved promises and mutation observers. After the current operation in the call stack is complete, the event loop processes all microtasks before moving on to macrotasks.
Example with Promises:
console.log("Script Start");
setTimeout(() => console.log("Macrotask: setTimeout"), 0);
Promise.resolve().then(() => console.log("Microtask: Promise"));
console.log("Script End");
Output:
Script Start
Script End
Microtask: Promise
Macrotask: setTimeout
Explanation:
- The synchronous code (
console.log
) runs first. - The
setTimeout
callback is queued in the macrotask queue. - The
Promise
callback is queued in the microtask queue. - Microtasks are executed first, so
Microtask: Promise
logs before thesetTimeout
callback.
Macrotask Queue
The Macrotask Queue handles tasks like setTimeout
, setInterval
, and I/O events. These tasks are processed after the microtask queue is empty.
Example with Timers:
console.log("Script Start");
setTimeout(() => console.log("Macrotask 1: setTimeout"), 0);
setTimeout(() => console.log("Macrotask 2: setTimeout"), 0);
console.log("Script End");
Output:
Script Start
Script End
Macrotask 1: setTimeout
Macrotask 2: setTimeout
Explanation:
- Synchronous code runs first.
- Both
setTimeout
callbacks are added to the macrotask queue. - The event loop processes them in FIFO order after the call stack is empty.
How They Work Together
Here’s the sequence:
- Call Stack Execution: Functions execute one at a time, in order.
- Microtask Processing: Once the stack is empty, the event loop processes all tasks in the microtask queue.
- Macrotask Processing: When the microtask queue is empty, the event loop processes tasks from the macrotask queue.
Combined Example:
console.log("Script Start");
setTimeout(() => console.log("Macrotask: setTimeout"), 0);
Promise.resolve()
.then(() => console.log("Microtask 1: Promise"))
.then(() => console.log("Microtask 2: Promise"));
console.log("Script End");
Output:
Script Start
Script End
Microtask 1: Promise
Microtask 2: Promise
Macrotask: setTimeout
Explanation:
- The synchronous
console.log
calls execute first. - The
setTimeout
callback goes to the macrotask queue. - The first
Promise
resolution goes to the microtask queue, followed by the second.then()
. - Microtasks are executed before the macrotask, ensuring
Microtask 1
andMicrotask 2
finish first.
Visualizing the Event Loop
- Call Stack: Functions are added and removed like stacking dishes.
- Microtask Queue: High-priority tasks (e.g., promises) are queued and executed first.
- Macrotask Queue: Lower-priority tasks (e.g., timers) are executed after microtasks.
Key Takeaways
- Event Loop ensures JavaScript is non-blocking and responsive.
- Microtasks (e.g., promises) always execute before macrotasks (e.g., timers).
- Understanding task prioritization helps avoid unexpected behavior in asynchronous code.
Conclusion
By understanding the Call Stack, Event Loop, Microtask Queue, and Macrotask Queue, you can write efficient and responsive JavaScript code. Use these concepts to debug, optimize, and handle complex asynchronous operations effectively.
Happy coding! 🎉