JavaScript Event Loop
The Event Loop is a fundamental concept in JavaScript that explains how asynchronous operations are handled. Understanding it is crucial for writing efficient JavaScript code and debugging asynchronous issues.
What is the Event Loop?
The Event Loop is JavaScript's mechanism for handling asynchronous operations. It continuously monitors the call stack and task queues, moving tasks from queues to the call stack when the stack is empty.
Key Components
1. Call Stack
- LIFO (Last In, First Out) data structure
- Stores function calls and their execution context
- Only one thread of execution
2. Web APIs / Node.js APIs
- Browser APIs (setTimeout, DOM events, HTTP requests)
- Node.js APIs (fs, http, timers)
- Handle asynchronous operations
3. Task Queues
- Macrotask Queue: setTimeout, setInterval, I/O operations
- Microtask Queue: Promises, queueMicrotask, MutationObserver
4. Event Loop
- Continuously checks if call stack is empty
- Moves tasks from queues to call stack
Execution Order
console.log('1'); // Synchronous
setTimeout(() => console.log('2'), 0); // Macrotask
Promise.resolve().then(() => console.log('3')); // Microtask
console.log('4'); // Synchronous
// Output: 1, 4, 3, 2
Detailed Example
console.log('Start');
setTimeout(() => console.log('Timeout 1'), 0);
setTimeout(() => console.log('Timeout 2'), 0);
Promise.resolve().then(() => console.log('Promise 1'));
Promise.resolve().then(() => console.log('Promise 2'));
console.log('End');
// Output:
// Start
// End
// Promise 1
// Promise 2
// Timeout 1
// Timeout 2
Microtasks vs Macrotasks
Microtasks (Higher Priority)
- Promise.then/catch/finally
- queueMicrotask()
- MutationObserver
- Process.nextTick() (Node.js)
Macrotasks (Lower Priority)
- setTimeout/setInterval
- setImmediate() (Node.js)
- I/O operations
- UI rendering
Complex Example
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
console.log('3');
return Promise.resolve();
}).then(() => {
console.log('4');
});
setTimeout(() => console.log('5'), 0);
Promise.resolve().then(() => console.log('6'));
console.log('7');
// Output: 1, 7, 3, 4, 6, 2, 5
Event Loop in Action
function eventLoopDemo() {
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => {
console.log('C');
setTimeout(() => console.log('D'), 0);
});
console.log('E');
}
eventLoopDemo();
// Step-by-step execution:
// 1. A (synchronous)
// 2. setTimeout B queued (macrotask)
// 3. Promise C queued (microtask)
// 4. E (synchronous)
// 5. Call stack empty, process microtasks
// 6. C (microtask)
// 7. setTimeout D queued (macrotask)
// 8. Process macrotasks
// 9. B (macrotask)
// 10. D (macrotask)
// Output: A, E, C, B, D
Common Pitfalls
1. Blocking the Event Loop
// Bad - blocks the event loop
function blockingFunction() {
const start = Date.now();
while (Date.now() - start < 5000) {
// Blocking for 5 seconds
}
console.log('Blocking done');
}
// Good - non-blocking
function nonBlockingFunction() {
setTimeout(() => {
console.log('Non-blocking done');
}, 5000);
}
2. Infinite Microtask Loop
// This will block the event loop
function infiniteMicrotasks() {
Promise.resolve().then(() => {
console.log('Microtask');
infiniteMicrotasks(); // Recursive call
});
}
// Better approach
function controlledMicrotasks() {
let count = 0;
function scheduleNext() {
if (count < 10) {
Promise.resolve().then(() => {
console.log('Microtask', count++);
scheduleNext();
});
}
}
scheduleNext();
}
Browser vs Node.js
Browser Event Loop
- Single-threaded
- Web APIs handle I/O
- Microtasks processed between each macrotask
Node.js Event Loop
- Multi-phase event loop
- libuv handles I/O
- Different phases: timers, pending callbacks, idle, poll, check, close
// Node.js specific
setImmediate(() => console.log('setImmediate'));
setTimeout(() => console.log('setTimeout'), 0);
process.nextTick(() => console.log('nextTick'));
// Output: nextTick, setTimeout, setImmediate
Practical Applications
1. Debouncing
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
const debouncedSearch = debounce((query) => {
console.log('Searching for:', query);
}, 300);
2. Throttling
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
const throttledScroll = throttle(() => {
console.log('Scroll event');
}, 100);
3. Async Queue Processing
class AsyncQueue {
constructor() {
this.queue = [];
this.processing = false;
}
add(task) {
this.queue.push(task);
this.process();
}
async process() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
const task = this.queue.shift();
await task();
}
this.processing = false;
}
}
Key Concepts
- Single-threaded: JavaScript has one call stack
- Non-blocking: Asynchronous operations don't block the main thread
- Event-driven: Operations are triggered by events
- Queue Priority: Microtasks have higher priority than macrotasks
- Continuous Loop: Event loop runs continuously
Common Interview Questions
- Explain the JavaScript Event Loop
- What's the difference between microtasks and macrotasks?
- What's the output of this code? (Event loop timing questions)
- How does setTimeout work internally?
- What happens when you have an infinite microtask loop?
- How does the event loop differ between browser and Node.js?
Related Topics
- Promises and Async/Await
- Callbacks and Higher-Order Functions
- Web APIs and Browser APIs
- Node.js Event Loop
- Concurrency and Parallelism