JavaScript is known for its single-threaded nature, yet it handles asynchronous operations efficiently. This is made possible by the event loop, which manages the execution of synchronous code, asynchronous callbacks, and promises. To fully understand how JavaScript handles asynchronous code, you need to know about macrotasks and microtasks.
In this post, weβll explore the event loop, how macrotasks and microtasks work, and how they affect the execution of your JavaScript code.
π What is the Event Loop?
The event loop is the mechanism that allows JavaScript to perform non-blocking operations by offloading tasks to the browser or runtime environment and executing them when the main thread is free.
Key Points:
- JavaScript runs code in a single thread.
- The event loop coordinates between the call stack, task queue, and microtask queue.
- It ensures that tasks are executed in the correct order, maintaining smooth application performance.
β‘ Macrotasks vs Microtasks
π Macrotasks
Macrotasks include:
setTimeout
setInterval
-
setImmediate
(Node.js) - I/O tasks
- UI rendering tasks
When Are They Executed?
- Macrotasks are queued in the task queue.
- After the call stack is empty, the event loop picks the first macrotask and executes it.
Example:
console.log('Script start');
setTimeout(() => {
console.log('Macrotask: setTimeout');
}, 0);
console.log('Script end');
Output:
Script start
Script end
Macrotask: setTimeout
β‘ Microtasks
Microtasks include:
- Promises (
.then
,.catch
,.finally
) -
process.nextTick()
(Node.js) MutationObserver
When Are They Executed?
- Microtasks are queued in the microtask queue.
- After each operation on the call stack completes, all microtasks in the queue are executed before any macrotask is processed.
Example:
console.log('Script start');
Promise.resolve().then(() => {
console.log('Microtask: Promise.resolve');
});
console.log('Script end');
Output:
Script start
Script end
Microtask: Promise.resolve
π Macrotasks vs Microtasks: Execution Order
Letβs see how they interact:
console.log('Script start');
setTimeout(() => {
console.log('Macrotask: setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Microtask: Promise.resolve');
});
console.log('Script end');
Output:
Script start
Script end
Microtask: Promise.resolve
Macrotask: setTimeout
Why? Because:
- Synchronous code runs first.
- After the call stack is empty, all microtasks are processed.
- Only then are macrotasks executed.
π Real-World Example: Understanding Task Prioritization
console.log('Script start');
setTimeout(() => {
console.log('Macrotask: setTimeout 1');
}, 0);
Promise.resolve().then(() => {
console.log('Microtask: Promise 1');
});
setTimeout(() => {
console.log('Macrotask: setTimeout 2');
}, 0);
Promise.resolve().then(() => {
console.log('Microtask: Promise 2');
});
console.log('Script end');
Output:
Script start
Script end
Microtask: Promise 1
Microtask: Promise 2
Macrotask: setTimeout 1
Macrotask: setTimeout 2
π― Key Takeaways
- Synchronous code runs first.
- Microtasks are processed after each synchronous operation and before any macrotask.
- Macrotasks are executed one at a time after microtasks have cleared.
- Misunderstanding task order can lead to unexpected results, especially when dealing with promises and timers.
β¨ Conclusion
Understanding the event loop, along with the behavior of macrotasks and microtasks, is essential for writing efficient and bug-free JavaScript code. It ensures you manage asynchronous operations correctly, optimize performance, and avoid tricky bugs.
π¬ What challenges have you faced with the event loop? Share your experiences in the comments! π
Top comments (0)