Event loops in js

In JavaScript, the event loop is a fundamental concept that manages the execution of asynchronous operations and ensures that the program remains responsive. It is responsible for handling events, callbacks, and promises in a non-blocking manner.

The event loop ensures that synchronous code is executed in a blocking manner, while asynchronous code is executed in a non-blocking manner. This allows JavaScript to handle multiple tasks concurrently without blocking the main thread

Threads of Execution:

JavaScript is single-threaded, meaning it has only one thread of execution. A thread of execution refers to the sequence in which statements are executed in your code. JavaScript executes code line by line, one at a time, in a specific order.

Synchronous Code:

Synchronous code is code that runs sequentially, one line at a time. Each line of code is executed and completed before moving on to the next line. Synchronous operations block the execution of subsequent code until the current operation is finished.

console.log("Start")
console.log("Middle")
console.log("End")

In this code, each console.log statement will be executed in order: first ‘Start’, then ‘Middle’, and finally ‘End’. The execution of subsequent statements waits for the previous statement to finish.

Synchronous operations "block the execution of subsequent code"

This simply means that when a synchronous operation is running, it prevents the execution of any code that comes after it until it completes.

console.log("Start")
// Synchronous operation
for (let i = 0; i < 1000000000; i++) {
  // Simulating a time-consuming operation
}
console.log("End")

In this code, there is a loop that performs a time-consuming operation. While the loop is running, it consumes CPU cycles, and the subsequent console.log('End') statement is not executed until the loop finishes. The execution of the subsequent code is blocked until the synchronous operation is completed.

Asynchronous Code:

Asynchronous code allows multiple operations to be initiated, and their results are handled later without blocking the execution of subsequent code. It allows the program to continue running while waiting for long-running operations, such as network requests or file I/O, to complete.

Asynchronous code in JavaScript is typically achieved through the use of callbacks, Promises, or async/await syntax.

Callbacks: Callbacks are functions passed as arguments to other functions. They are executed at a later time when a certain event or condition occurs. Callback functions allow you to specify what should happen after an asynchronous operation completes.

function asyncOperation(callback) {
  setTimeout(() => {
    callback(null, "Async operation completed")
  }, 2000)
}
console.log("Start")
asyncOperation((error, result) => {
  if (error) {
    console.error("Error:", error)
  } else {
    console.log(result)
  }
})
console.log("End")

In this example, the asyncOperation function simulates an asynchronous operation by using setTimeout to delay the execution. It takes a callback function as an argument and calls it once the operation is complete. The console.log('Start') and console.log('End') statements are executed immediately, while the callback is called asynchronously after a 2-second delay.

Promises: Promises provide a more structured way to handle asynchronous code. A Promise represents the eventual completion or failure of an asynchronous operation and allows you to attach callbacks to handle the success or failure of that operation.

function asyncOperation() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Async operation completed")
    }, 2000)
  })
}
 
console.log("Start")
 
asyncOperation()
  .then((result) => {
    console.log(result)
  })
  .catch((error) => {
    console.error("Error:", error)
  })
console.log("End")

In this example, the asyncOperation function returns a Promise that resolves after a 2-second delay. The console.log('Start') and console.log('End') statements are executed synchronously. The then method is used to handle the resolved value of the Promise, while the catch method is used to handle any potential errors.

Async/Await: Async/await is a modern syntax introduced in ES2017 (ES8) that provides a more concise and readable way to write asynchronous code. It allows you to write asynchronous code in a more synchronous-like manner using the async and await keywords.

function delay(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms)
  })
}
 
async function asyncOperation() {
  await delay(2000)
  return "Async operation completed"
}
 
console.log("Start")
;(async () => {
  try {
    const result = await asyncOperation()
    console.log(result)
  } catch (error) {
    console.error("Error:", error)
  }
})()
 
console.log("End")

In this example, the asyncOperation function is an asynchronous function that uses the await keyword to pause the execution until the delay Promise resolves. The function returns a value once the delay is completed. The outer function is an immediately-invoked async function that wraps the asynchronous code. The try/catch block is used to handle any potential errors. The console.log('Start') and console.log('End') statements are executed synchronously.

The event loop ensures that synchronous code is executed in a blocking manner, while asynchronous code is executed in a non-blocking manner. This allows JavaScript to handle multiple tasks concurrently without blocking the main thread.

It's important to note that long-running synchronous operations can block the event loop and make the application unresponsive. To avoid this, time-consuming tasks should be offloaded to Web Workers or broken down into smaller chunks using techniques like asynchronous programming or Promises

Event Loop Overview

Here's an overview of how the event loop works in JavaScript:

  • 🔥 Single-threaded Nature:
    • 💡 JavaScript is single-threaded, meaning it has only one thread of execution. This thread is responsible for executing your JavaScript code sequentially.
  • 🔥 Call Stack:
    • 💡 The call stack is a data structure that keeps track of function calls in progress. When a function is called, it is pushed onto the stack, and when it completes, it is popped off the stack.
  • 🔥 Event Queue:
    • 💡 The event queue is a queue that holds events and callbacks. When an asynchronous event or callback is triggered, it is added to the event queue.
  • 🔥 Event Loop:
    • 💡 The event loop continuously checks the call stack and the event queue. If the call stack is empty, it takes the next event or callback from the event queue and pushes it onto the call stack for execution.
  • 🔥 Non-Blocking Behavior:
    • 💡 Asynchronous operations in JavaScript, such as setTimeout, AJAX requests, or Promise callbacks, are handled by the event loop. These operations are initiated, and their associated callbacks are scheduled to be executed later when the call stack is empty.
  • 🔥 Event-driven Programming - 💡 JavaScript is event-driven, meaning it responds to events like user actions, timers, and network responses. When an event occurs, the corresponding callback is added to the event queue and eventually executed by the event loop.

    The important thing to understand is that in JavaScript, regardless of whether the code is synchronous or asynchronous, it still runs on a single thread of execution. Asynchronous operations are managed by the event loop, which schedules and handles the execution of callbacks and Promises when they are ready.

JavaScript Queues

### In JavaScript, there are typically four main queues that are commonly referred to:

  1. [>] Event Queue (also known as Task Queue):
    • [/] This queue stores events and their corresponding callback functions. Events include things like DOM events (e.g., click, keypress) and other asynchronous tasks (e.g., timers, network requests). The callback functions in the event queue are processed by the Event Loop when the call stack is empty.
  2. [>] Callback Queue (also known as Message Queue):
    • [/] This queue is similar to the event queue and stores the callbacks of completed asynchronous tasks, such as timers and network requests. These callbacks are enqueued in the callback queue when their respective tasks are finished, and they are executed by the Event Loop when the call stack is empty.
  3. [>] Microtask Queue (also known as Promise Queue):
    • [/] This queue is specifically used for handling microtasks. Microtasks are tasks with a higher priority than regular tasks and are usually created by resolving Promises. Microtasks are executed before the next cycle of the event loop begins, prioritizing them over regular tasks in the callback queue.
  4. [>] Animation Frame Queue:
    • [/] This queue is used to schedule animations for smooth rendering. It allows developers to schedule functions to be executed before the browser performs the next repaint. Functions scheduled using requestAnimationFrame are enqueued in the animation frame queue.

The Event Loop plays a crucial role in managing the flow of tasks between these queues and the call stack, ensuring asynchronous operations are executed efficiently without blocking the main thread.

ES5 Web Browser APIs with callback functions

  • 💡 Problems
    • ➡️ Our response data is only available in the callback function - Callback hell
    • ➡️ Maybe it feels a little odd to think of passing a function into another function only for it to run much later
  • 💡 Benefits
    • ➡️ Super explicit once you understand how it works under-the-hood

Rules to executing Asynchronous code

When running asynchronous code in JavaScript, there are rules that dictate the order in which tasks are executed in relation to the event queue, microtask queue, and callback queue. These rules are essential for understanding how the Event Loop handles asynchronous operations. Here's a general outline of the rules:

  • [/] Synchronous Code Execution:
    • 💡 JavaScript executes synchronous code in the order it appears in the script. Functions are called and executed, and the call stack keeps track of the function calls.
  • [/] Asynchronous Code Execution:
    • 💡 When asynchronous operations (e.g., timers, network requests, user events) are encountered, they are moved out of the regular execution flow, and their callbacks are placed in the corresponding queues.
  • [/] Event Queue and Callback Queue:
    • 💡 The event queue (task queue) and the callback queue (message queue) are two separate queues. Events and their corresponding callbacks are placed in the event queue, while callbacks of completed asynchronous tasks (e.g., timers, network requests) are placed in the callback queue.
  • [/] Microtask Queue:
    • 💡 The microtask queue is separate from the event queue and callback queue. Microtasks are typically created by resolving Promises. Microtasks have higher priority than regular tasks in the event queue.
  • [/] Event Loop:
    • 💡 The Event Loop continuously checks the call stack and, when it’s empty, starts processing tasks from the queues.

The rule of thumb for task execution order is as follows:

  • 🔥 Synchronous tasks are executed in the order they appear in the script.

  • 🔥 When the call stack is empty (no synchronous tasks left to execute), the Event Loop follows these steps:

    • ➡️ a. It first checks the microtask queue. If there are any microtasks, they are all executed in the order they were added, one after the other, until the microtask queue is empty.
    • ➡️ b. After processing all microtasks, the Event Loop checks the animation frame queue. If there are any animation frame requests, it processes them in the order they were added.
    • ➡️ c. If there are no animation frame requests, the Event Loop moves on to the regular task queue (event queue) and processes the oldest task (callback) in the queue. After processing one task, the Event Loop checks the microtask queue again before moving to the next task in the event queue.
    • ➡️ d. The Event Loop repeats this process, going through the microtask queue before each task in the event queue, until both queues are empty.

This process ensures that microtasks have higher priority than regular tasks in the event queue, and it helps keep asynchronous operations responsive and efficient in JavaScript applications.

#### We have rules for the execution of our asynchronously delayed code

Hold promise-deferred functions in a microtask queue and callback function in a task queue (Callback queue) when the Web Browser Feature (API) finishes Add the function to the Call stack (i.e. run the function) when:

  • ➡️ Call stack is empty & all global code run (Have the Event Loop check this condition) Prioritize functions in the microtask queue over the Callback queue

What goes where ?

In JavaScript, different tasks are placed in specific queues based on their nature and priority. Here’s a breakdown of the tasks that go into the microtask queue, callback queue, and event queue:

  1. [i] Microtask Queue (Promise Queue):

    • ✅ Tasks placed in the microtask queue have higher priority than regular tasks in the callback queue. They are executed before regular tasks during the Event Loop.
    • [/] Things that go into the microtask queue:
      • ➡️ Promise callbacks: When a Promise is resolved (fulfilled or rejected), its respective then() or catch() callbacks are enqueued in the microtask queue. This ensures that Promise handlers are executed before regular tasks in the callback queue.
      • ➡️ Mutation Observer: Mutation Observer callbacks are enqueued in the microtask queue when DOM mutations are observed. This allows these callbacks to be executed before the next rendering cycle, ensuring that any DOM changes are handled consistently.
  2. [i] Callback Queue (Task Queue or Message Queue):

    • ✅ The callback queue holds callbacks of completed asynchronous tasks, such as timers, network requests, and user interactions.
    • [/] Things that go into the callback queue:
      • ➡️ setTimeout and setInterval: When the specified time for a timer (created using setTimeout or setInterval) elapses, the associated callback is placed in the callback queue.
      • ➡️ Network Requests: When an asynchronous network request (e.g., AJAX or fetch) is completed, its corresponding callback is enqueued in the callback queue.
      • ➡️ User Interaction Events: Events like button clicks, keyboard inputs, and other user interactions trigger their corresponding callback functions, which are placed in the callback queue.
  3. [i] Event Queue (Task Queue):

    • ✅ The event queue stores events and their corresponding callback functions. It is used for handling various asynchronous events, including DOM events and other types of events.
    • [/] Things that go into the event queue:
      • ➡️ DOM Events: When an event (e.g., click, keypress) occurs on a DOM element, the event and its associated callback (event handler) are placed in the event queue.
      • ➡️ Asynchronous Tasks: Other asynchronous tasks, such as timers and scheduled tasks, are placed in the event queue with their respective callbacks.

In summary

  • 🔥 Microtask Queue (Promise Queue): For high-priority tasks like Promise callbacks and Mutation Observer callbacks.
  • 🔥 Callback Queue (Task Queue or Message Queue): For regular asynchronous tasks like timers, network requests, and user interaction events.
  • 🔥 Event Queue (Task Queue): For various asynchronous events, including DOM events and other types of events.

What Really happens under the hood.

We have all heard about JavaScript and Node.js being single-threaded, but what does it mean in practical terms?

It means that JavaScript can do one thing at a time. For example, we cannot simultaneously multiply and sum numbers. We usually do operations in sequence. We add and then multiply or vice versa. Modern computers are fast, and the result of two or more consecutive tasks seems to be computed simultaneously, but there are exceptions.

We all have tried to scrape data from that slow website or waited more than thirty seconds before getting the result of a database query. Do we want to block our single thread from executing more tasks because of a slow database query? Luckily, Node.js doesn’t stop from running other operations because of Libuv, a C++ library responsible for the event loop and asynchronously handling tasks such as network requests, DNS resolution, file system operations, data encryption, etc.

We have god our codes that we need to execute.

In our example, the line of code console.log('Starting Node.js') is added to the call stack and prints Starting Node.js to the console. By doing so, it reaches the end of the log function and is removed from the call stack.

The following line of code is a database query. These tasks are immediately popped off because they may take a long time. They are passed to Libuv, which asynchronously handles them in the background. At the same time, Node.js can keep running other code without blocking its single thread.

While Libuv handles the query in the background, our JavaScript is not blocked and can continue with console.log(”Before query result”).

When the query is done, its callback is pushed to the I/O Event Queue to be run shortly**.** The event loop connects the queue with the call stack. It checks if the latter is empty and moves the first queue item for execution.

The event loop, the delegation, and the asynchronous processing mechanism are Node.js's secret ingredients to process thousands of connections, read/write gigantic files, handling timers while working on other parts of our code.