Understanding the Event Loop in JavaScript

You probably already know that JavaScript is a single-threaded programming language. This means that JavaScript runs on a single main thread within web browsers or Node.js. Running on a single main thread means that only one piece of JavaScript code runs at a time.

The event loop in JavaScript plays an important role in determining how code executes on the main thread. The event loop takes care of a few things such as the execution of code and the collection and processing of events. It also handles the execution of any queued sub-tasks.

In this tutorial, you’ll learn the basics of the event loop in JavaScript.

How the Event Loop Works

There are three important terms that you need to know in order to understand how the event loop works.

Stack

The call stack is simply a stack of function calls that tracks the execution context of functions. This stack follows the last-in-first-out (LIFO) principle, which means that the most recently invoked function will be the first one which is executed.

Queue

The queue contains a list of tasks that are in line to be executed by JavaScript. The tasks in this queue can result in the invocation of functions which will then be placed on the stack. The processing of the queue starts only when the stack is empty. The items in the queue follow the first-in-first-out (FIFO) principle. This means that the oldest tasks will be completed first.

Heap

The heap is basically a large region of memory where objects are stored and allocated. Its primary purpose is the storage of data which might be used by functions in the stack.

Basically, JavaScript is single-threaded and executes one function at a time. This single function is placed on the stack. The said function can also contain other nested functions which will be placed above it in the stack. The stack follows the LIFO principle, so the nested functions which were invoked most recently are executed first.

Asynchronous tasks like API requests or a timer are added to the queue for execution at a later time. The JavaScript engine starts executing the tasks in the queue when it is sitting idle.

Consider the following example:

1
function helloWorld() {
2
    console.log("Hello, World!");
3
}
4

5
function helloPerson(name) {
6
    console.log(`Hello, ${name}!`);
7
}
8

9
function helloTeam() {
10
    console.log("Hello, Team!");
11
    helloPerson("Monty");
12
}
13

14
function byeWorld() {
15
    console.log("Bye, World!");
16
}
17

18
helloWorld();
19
helloTeam();
20
byeWorld();
21

22
/* Outputs:
23

24
Hello, World!
25
Hello, Team!
26
Hello, Monty!
27
Bye, World!
28

29
*/

Let’s see what the stack and queue would look like if we ran the code above.

The helloWorld() function is invoked and put on the stack. It logs Hello, World! which completes its execution, so it is taken off the stack. The helloTeam() function is invoked next and put on the stack. During its execution, we log Hello, Team! and invoke helloPerson(). The execution of helloTeam() still isn’t complete, so it stays on the stack, and helloPerson() is placed above it.

The LIFO principle dictates that helloPerson() executes now. This logs Hello, Monty! to the console, which completes its execution, and helloPerson() is taken off the stack. The helloTeam() function goes off the stack after that, and we finally get to byeWorld(). It logs Bye, World! and then goes off the stack.

The queue stays empty this whole time.

Now, consider a slight variation of the above code:

1
function helloWorld() {
2
    console.log("Hello, World!");
3
}
4

5
function helloPerson(name) {
6
    console.log(`Hello, ${name}!`);
7
}
8

9
function helloTeam() {
10
    console.log("Hello, Team!");
11
    setTimeout(() => {
12
        helloPerson("Monty");
13
    }, 0);
14
}
15

16
function byeWorld() {
17
    console.log("Bye, World!");
18
}
19

20
helloWorld();
21
helloTeam();
22
byeWorld();
23

24
/* Outputs:
25

26
Hello, World!
27
Hello, Team!
28
Bye, World!
29
Hello, Monty!
30

31
*/

The only change we made here was the use of setTimeout(). However, the timeout has been set to zero. Therefore, we would expect Hello, Monty! to be output before Bye, World! You can see why that doesn’t happen if you understand how the event loop works.

When helloTeam() is on the stack, it encounters the setTimeout() method. However, the call to helloPerson() within setTimeout() is placed on the queue to be executed once there are no synchronous tasks to be executed.

Once the call to byeWorld() has completed, the event loop checks if there are any pending tasks in the queue, and it finds the call to helloPerson(). At this point, it executes the function and logs Hello, Monty! to the console.

This shows that the timeout duration that you provide to setTimeout() isn’t the guaranteed time when the callback will execute. It is the minimum time after which the callback will execute.

Keeping Our Webpage Responsive

One interesting feature of JavaScript is that it runs a function until its completion. This means that as long as a function is on the stack, the event loop won’t be able to tackle any other tasks in the queue or execute other functions.

This can cause the webpage to “hang” as it won’t be able to do other things like handling user input or making DOM-related changes. Consider the following example, where we find the number of primes in a given range:

1
function isPrime(num) {
2
  if (num <= 1) {
3
    return false;
4
  }
5

6
  for (let i = 2; i <= Math.sqrt(num); i++) {
7
    if (num % i === 0) {
8
      return false;
9
    }
10
  }
11
  
12
  return true;
13
}
14

15
function listPrimesInRange(start, end) {
16
  const primes = [];
17

18
  for (let num = start; num <= end; num++) {
19
    if (isPrime(num)) {
20
      primes.push(num);
21
    }
22
  }
23

24
  return primes;
25
}

Within our listPrimesInRange() function, we iterate over numbers ranging from start to end. For each of these numbers, we call the isPrime() function to see if it is a prime. The isPrime() function itself has a for loop that goes from 2 to Math.sqrt(num) to figure out if the number is prime.

Finding all the primes in a given range can take a while, depending on the values you use. While the browser is doing this calculation, it won’t be able to do anything else. This is because the listPrimesInRange() function will stay on the stack, and the browser won’t be able to execute any other tasks in the queue.

Now, take a look at the following function:

1
function listPrimesInRangeResponsively(start) {
2
  let next = start + 100,000;
3

4
  if (next > end) {
5
    next = end;
6
  }
7

8
  for (let num = start; num <= next; num++) {
9
    if (isPrime(num)) {
10
      primeNumbers.push(num);
11
    }
12

13
    if (num == next) {
14
      percentage = ((num - begin) * 100) / (end - begin);
15
      percentage = Math.floor(percentage);
16

17
      progress.innerText = `Progress ${percentage}%`;
18

19
      if (num != end) {
20
        setTimeout(() => {
21
          listPrimesInRangeResponsively(next + 1);
22
        });
23
      }
24
    }
25

26
    if (num == end) {
27
      percentage = ((num - begin) * 100) / (end - begin);
28
      percentage = Math.floor(percentage);
29

30
      progress.innerText = `Progress ${percentage}%`;
31

32
      heading.innerText = `${primeNumbers.length - 1} Primes Found!`;
33

34
      console.log(primeNumbers);
35

36
      return primeNumbers;
37
    }
38
  }
39
}

This time, our function only tries to find the primes while processing the range in batches. It does so by going through all the numbers but processing only 100,000 of them at a time. After that, it uses setTimeout() to trigger the next call to the same function.

When setTimeout() is called without a delay specified, it adds the callback function to the event queue right away.

This next call is placed in the queue, emptying the stack momentarily to handle any other tasks. After that, the JavaScript engine starts finding the primes in the next batch of 100,000 numbers.

Try clicking the Calculate (Stuck) button on this page, and you’re likely to get a message saying that the webpage is slowing down your browser and recommending that you stop the script.

On the other hand, a click on the Calculate (Responsive) button will still keep the webpage responsive.

Final Thoughts

In this tutorial, we learned about the event loop in JavaScript and how it executes synchronous and asynchronous code efficiently. The event loop uses the queue to keep track of the tasks it has to do.

Since JavaScript keeps executing a function till completion, doing a lot of computations can sometimes “hang” the browser window. With our understanding of the event loop, we can rewrite our functions so that they do their computations in batches. This allows the browser to keep the window responsive for the users. It also allows us to regularly update users about the progress we have made in our computations.


Source link