🔄 The JavaScript Event Loop is a fundamental concept that powers asynchronous programming in JavaScript. Let's break it down with real-world examples and practical applications.
What is the Event Loop?
The Event Loop is JavaScript's mechanism for executing code, handling events, and processing asynchronous operations. It consists of several key components:
- Call Stack
- Callback Queue (Macrotask Queue)
- Microtask Queue
- Event Loop
The Call Stack
The call stack is where JavaScript executes synchronous code:
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
function printSquare(n) {
const result = square(n);
console.log(result);
}
printSquare(4); // Call stack: printSquare -> square -> multiply
Callback Queue (Macrotask Queue)
Handles setTimeout, setInterval, and DOM events:
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
}, 0);
setTimeout(() => {
console.log('Timeout 2');
}, 0);
console.log('End');
// Output:
// Start
// End
// Timeout 1
// Timeout 2
Microtask Queue
Handles Promises and queueMicrotask:
console.log('Start');
Promise.resolve().then(() => {
console.log('Promise 1');
});
queueMicrotask(() => {
console.log('Microtask 1');
});
Promise.resolve().then(() => {
console.log('Promise 2');
});
console.log('End');
// Output:
// Start
// End
// Promise 1
// Microtask 1
// Promise 2
Real-World Examples
1. User Interface Updates
class UIUpdater {
constructor() {
this.pendingUpdates = new Set();
this.isUpdating = false;
}
scheduleUpdate(elementId, updateFn) {
this.pendingUpdates.add({ elementId, updateFn });
if (!this.isUpdating) {
this.processUpdates();
}
}
async processUpdates() {
this.isUpdating = true;
// Process in next microtask
await Promise.resolve();
for (const update of this.pendingUpdates) {
try {
const element = document.getElementById(update.elementId);
if (element) {
update.updateFn(element);
}
} catch (error) {
console.error('Update failed:', error);
}
}
this.pendingUpdates.clear();
this.isUpdating = false;
}
}
// Usage
const updater = new UIUpdater();
updater.scheduleUpdate('counter', (element) => {
element.textContent = 'Updated!';
});
2. API Request Queue
class RequestQueue {
constructor(concurrency = 3) {
this.concurrency = concurrency;
this.queue = [];
this.active = 0;
}
async add(request) {
this.queue.push(request);
await this.process();
}
async process() {
if (this.active >= this.concurrency || this.queue.length === 0) {
return;
}
this.active++;
const request = this.queue.shift();
try {
const response = await fetch(request.url, request.options);
const data = await response.json();
request.resolve(data);
} catch (error) {
request.reject(error);
} finally {
this.active--;
await this.process();
}
}
enqueue(url, options = {}) {
return new Promise((resolve, reject) => {
this.add({ url, options, resolve, reject });
});
}
}
// Usage
const queue = new RequestQueue();
async function fetchData() {
try {
const results = await Promise.all([
queue.enqueue('/api/users'),
queue.enqueue('/api/posts'),
queue.enqueue('/api/comments'),
queue.enqueue('/api/likes')
]);
console.log('All requests completed:', results);
} catch (error) {
console.error('Request failed:', error);
}
}
3. Animation Frame Manager
class AnimationManager {
constructor() {
this.animations = new Map();
this.frameId = null;
}
add(key, animation) {
this.animations.set(key, {
frame: 0,
animation
});
this.start();
}
remove(key) {
this.animations.delete(key);
if (this.animations.size === 0) {
this.stop();
}
}
start() {
if (!this.frameId) {
this.loop();
}
}
stop() {
if (this.frameId) {
cancelAnimationFrame(this.frameId);
this.frameId = null;
}
}
loop() {
this.frameId = requestAnimationFrame(() => {
for (const [key, anim] of this.animations) {
try {
const finished = anim.animation(anim.frame++);
if (finished) {
this.remove(key);
}
} catch (error) {
console.error('Animation error:', error);
this.remove(key);
}
}
if (this.animations.size > 0) {
this.loop();
}
});
}
}
// Usage
const animator = new AnimationManager();
animator.add('fadeOut', (frame) => {
const element = document.getElementById('target');
const opacity = 1 - (frame / 60);
if (opacity <= 0) {
element.style.opacity = 0;
return true; // Animation complete
}
element.style.opacity = opacity;
return false; // Continue animation
});
Event Loop Patterns
1. Debouncing
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Usage
const debouncedSearch = debounce((query) => {
console.log('Searching for:', query);
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
2. Throttling
function throttle(fn, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Usage
const throttledScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 100);
window.addEventListener('scroll', throttledScroll);
Best Practices
- Avoid Blocking the Main Thread
// Bad
function heavyComputation() {
for (let i = 0; i < 1000000; i++) {
// Expensive operation
}
}
// Good
async function heavyComputation() {
const chunkSize = 1000;
let processed = 0;
while (processed < 1000000) {
await new Promise(resolve => setTimeout(resolve, 0));
for (let i = 0; i < chunkSize; i++) {
// Process chunk
processed++;
}
}
}
- Use RequestAnimationFrame for Animations
function animate() {
// Update animation
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
- Handle Promise Errors
Promise.resolve()
.then(() => {
throw new Error('Async error');
})
.catch(error => {
console.error('Caught:', error);
});
Conclusion
Understanding the Event Loop is crucial for writing efficient asynchronous JavaScript code. By properly managing tasks in the macrotask and microtask queues, you can create responsive applications that handle multiple operations smoothly without blocking the main thread.