10. Practical Examples: Combining Events, Stacks, and Queues
In this section, we’ll bring together the concepts of events, stacks, and queues in JavaScript to see how they work together in real-world scenarios. By understanding how these components interact, you’ll be better equipped to write efficient, non-blocking code and troubleshoot common issues in asynchronous programming.
Example 1: Event Handling with Callbacks and the Event Loop
Let’s start with a practical example that involves event handling, the call stack, and the callback queue.
Scenario:
You have a button on a web page that, when clicked, triggers a series of asynchronous tasks. The tasks involve logging a message, fetching data from an API (simulated with a setTimeout
), and updating the DOM with the result.
Example Code:
document.getElementById('myButton').addEventListener('click', function () {
console.log('Button clicked!');
setTimeout(() => {
console.log('Fetching data...');
setTimeout(() => {
console.log("Data fetched: { name: 'Lily' }");
document.getElementById('result').innerText = 'Name: Lily';
}, 2000);
}, 1000);
console.log('End of click handler');
});
Explanation:
- When the button is clicked, the click event listener is triggered, and the corresponding function is pushed onto the call stack.
- The function logs
Button clicked!
andEnd of click handler
synchronously. - The
setTimeout
function schedules the first asynchronous task (logging “Fetching data…”) to run after 1 second, placing its callback in the callback queue. - After 1 second, the first timeout callback logs “Fetching data…” and schedules another timeout for fetching the data, which logs
Data fetched: { name: 'Lily' }
after 2 more seconds and updates the DOM. - The output will be:
Button clicked!
End of click handler
Fetching data... (after 1 second)
Data fetched: { name: 'Lily' } (after 2 more seconds)
This example shows how asynchronous tasks are queued and processed in order, demonstrating the interaction between the call stack, event loop, and callback queue.
Example 2: Using Promises to Handle Asynchronous Operations
In this example, we’ll replace setTimeout
with promises to handle asynchronous operations more effectively. Promises offer a cleaner way to manage asynchronous tasks, particularly when dealing with complex chains of operations.
Scenario:
You want to fetch data asynchronously and then update the DOM with the fetched data, but this time using promises instead of setTimeout
.
Example Code:
function fetchData() {
return new Promise((resolve) => {
console.log('Fetching data...');
setTimeout(() => {
resolve({ name: 'Lily' });
}, 2000);
});
}
document.getElementById('myButton').addEventListener('click', function () {
console.log('Button clicked!');
fetchData().then((data) => {
console.log('Data fetched:', data);
document.getElementById('result').innerText = `Name: ${data.name}`;
});
console.log('End of click handler');
});
Explanation:
- The
fetchData
function returns a promise that resolves with the fetched data after 2 seconds. - The click event handler logs “Button clicked!” and “End of click handler” immediately.
- The
fetchData
function is called, and the returned promise is handled usingthen
, which logs the fetched data and updates the DOM. - The output will be:
Button clicked!
End of click handler
Fetching data...
Data fetched: { name: "Lily" } (after 2 seconds)
This example highlights how promises simplify asynchronous code by avoiding deeply nested callbacks (callback hell) and making the code more readable.
Example 3: Handling Multiple Asynchronous Tasks with Promise.all
Sometimes, you need to handle multiple asynchronous tasks concurrently and perform an action only after all tasks have completed. The Promise.all
method is ideal for this scenario.
Scenario: You want to fetch user details and user posts concurrently, and then update the DOM once both requests are complete.
Example Code:
function fetchUserDetails() {
return new Promise((resolve) => {
console.log('Fetching user details...');
setTimeout(() => {
resolve({ id: 1, name: 'Lily' });
}, 2000);
});
}
function fetchUserPosts() {
return new Promise((resolve) => {
console.log('Fetching user posts...');
setTimeout(() => {
resolve([{ postId: 1, content: 'Hello World!' }]);
}, 3000);
});
}
document.getElementById('myButton').addEventListener('click', function () {
console.log('Button clicked!');
Promise.all([fetchUserDetails(), fetchUserPosts()]).then(([userDetails, userPosts]) => {
console.log('User details:', userDetails);
console.log('User posts:', userPosts);
document.getElementById('result').innerText = `
Name: ${userDetails.name}, Posts: ${userPosts.length}`;
});
console.log('End of click handler');
});
Explanation:
- Two asynchronous tasks (
fetchUserDetails
andfetchUserPosts
) are executed concurrently usingPromise.all
. - The
Promise.all
method waits for both promises to resolve before executing thethen
block, ensuring that the DOM is only updated once all data is available. - The output will be:
Button clicked!
End of click handler
Fetching user details...
Fetching user posts...
User details: { id: 1, name: "Lily" } (after 2 seconds)
User posts: [{ postId: 1, content: "Hello World!" }] (after 3 seconds)
This example demonstrates how to handle multiple asynchronous operations concurrently and efficiently.
Example 4: Debugging Asynchronous Code with the Call Stack
Understanding the call stack is crucial for debugging asynchronous JavaScript code. In this example, we’ll see how to trace the execution flow when an error occurs.
Scenario: You have a series of asynchronous tasks, and an error occurs in one of them. You want to trace the error back to its source.
Example Code:
function fetchData() {
return new Promise((resolve, reject) => {
console.log('Fetching data...');
setTimeout(() => {
reject(new Error('Failed to fetch data'));
}, 2000);
});
}
document.getElementById('myButton').addEventListener('click', function () {
console.log('Button clicked!');
fetchData()
.then((data) => {
console.log('Data fetched:', data);
document.getElementById('result').innerText = `Name: ${data.name}`;
})
.catch((error) => {
console.error('Error:', error.message);
});
console.log('End of click handler');
});
Explanation:
- The
fetchData
function simulates a failed asynchronous operation by rejecting the promise after 2 seconds. - The error is caught in the
catch
block, which logs the error message to the console. - The output will be:
Button clicked!
End of click handler
Fetching data...
Error: Failed to fetch data (after 2 seconds)
In the browser’s developer tools, you would see the stack trace of the error, allowing you to trace back through the function calls to identify the source of the problem.
Summary:
These practical examples illustrate how events, stacks, and queues work together in JavaScript to manage asynchronous tasks. By combining these concepts, you can write more efficient, responsive, and maintainable code. Understanding the interaction between the event loop, callback queue, microtask queue, and the call stack is key to mastering asynchronous programming in JavaScript.
In the next section, we’ll recap the key concepts covered in this course and provide some final thoughts and tips for further learning.