top of page
Search
  • Writer's pictureMarko Reljić

Diving Deeper Into JavaScript's Event Loop with a Practical Example


To truly understand how JavaScript manages asynchronous operations alongside synchronous execution, let's dissect a practical code snippet. This detailed analysis will illuminate the intricacies of JavaScript's single-threaded nature, the event loop, and the prioritization between microtasks and macrotasks. The code we'll explore combines `setTimeout`, `Promise`, and synchronous operations to showcase the event loop in action.





The Code Snippet

console.log('First: Synchronous start');
setTimeout(() => {
  console.log('Eighth: setTimeout 3000ms');
}, 3000);

console.log('Second: Synchronous code');

let promise1 = new Promise((resolve, reject) => {
  console.log('Third: Promise 1 starts immediately');
  resolve('Fifth: Promise 1 resolved');
});

promise1.then((value) => {
  console.log(value);
});

let promise2 = new Promise((resolve, reject) => {
  console.log('Fourth: Promise 2 starts immediately');
  setTimeout(() => {
    resolve('Seventh: Promise 2 resolved after 2000ms');
  }, 2000);
});

promise2.then((value) => {
  console.log(value);
});
setTimeout(() => {
  console.log('Sixth: setTimeout 1000ms');
}, 1000);

console.log('Synchronous end');

Execution Analysis in Depth


1. Synchronous Code Execution

Initially, the JavaScript engine runs the code line-by-line in a synchronous manner.

- `'First: Synchronous start'` is logged immediately.

- Upon encountering the first `setTimeout`, it schedules a task for later but doesn't pause the execution. The function to log `'Eighth: setTimeout 3000ms'` is set to run after 3000 milliseconds.

- It continues to run synchronously, logging `'Second: Synchronous code'`.


2. Promises and Immediate Execution

- When the first `Promise` is declared, it immediately logs `'Third: Promise 1 starts immediately'`. The `resolve` function is called right away, but its effects (`then` clause) are deferred to the microtask queue.

- Similarly, the second `Promise` logs `'Fourth: Promise 2 starts immediately'`. However, its `resolve` function is wrapped in a `setTimeout`, indicating a delayed resolution.


3. Deferred Actions and the Microtask Queue

- The JavaScript engine continues, setting another `setTimeout` to log `'Sixth: setTimeout 1000ms'` after 1000 milliseconds.

- It then concludes the synchronous portion by logging `'Synchronous end'`.

After all synchronous code has run, the engine checks the microtask queue before moving to the next task. Here's where the behavior of promises and `setTimeout` diverge due to their handling by the JavaScript runtime:


4. Microtask Queue Processing

- The resolution of `Promise 1` is a microtask. Thus, it's processed immediately after the synchronous code, logging `'Fifth: Promise 1 resolved'`. Microtasks are prioritized over macrotasks (like those from `setTimeout`) to ensure that promises and other high-priority tasks are executed as soon as possible.


5. Macrotask Queue and `setTimeout` Callbacks

- The engine then processes macrotasks. The first `setTimeout` that logs `'Sixth: setTimeout 1000ms'` is executed after 1000 milliseconds from its scheduling. This is a simpler example of asynchronous JavaScript in action, demonstrating how `setTimeout` callbacks are queued as macrotasks.

- After 2000 milliseconds, the `setTimeout` inside `Promise 2` resolves, pushing its resolution to the microtask queue, which logs `'Seventh: Promise 2 resolved after 2000ms'`. Even though this involves a `setTimeout`, the actual promise resolution is treated as a microtask.

- Finally, the initial `setTimeout` scheduled for 3000 milliseconds logs `'Eighth: setTimeout 3000ms'`.



Why JavaScript Works This Way


JavaScript's single-threaded nature means it can only run one piece of code at a time. The event loop, along with the microtask and macrotask queues, allows JavaScript to perform non-blocking operations efficiently. This system ensures that high-priority tasks (like DOM updates and promise resolutions) are handled promptly, while still allowing for long-running tasks to be scheduled without freezing the web page.

Understanding Task Prioritization

- Microtasks include promise resolutions, `MutationObserver` callbacks, and other operations that should be executed immediately after the current script and before the next event loop iteration.

- Macrotasks include `setTimeout`, `setInterval`, I/O operations, and other callbacks that can be scheduled to run after all microtasks have been processed.

Key Takeaways

- Synchronous code blocks until complete, while asynchronous operations are scheduled

for future execution.

- Promises and their resolutions are processed as microtasks, ensuring their prompt handling.

- `setTimeout` and similar asynchronous APIs introduce tasks that are queued as macrotasks, executed after microtasks.

- The event loop facilitates this orchestration, enabling JavaScript to remain responsive and efficient despite its single-threaded constraints.


Through this detailed walkthrough, we've seen how JavaScript's event loop manages a delicate balance between executing synchronous code, handling promise resolutions promptly as microtasks, and scheduling asynchronous callbacks as macrotasks. Understanding this fundamental aspect of JavaScript is crucial for writing efficient, non-blocking code and leveraging the full power of asynchronous programming in web development.

35 views0 comments

Recent Posts

See All

Comments


bottom of page