7

Understanding the Event Loop in JavaScript

 1 year ago
source link: https://code.tutsplus.com/tutorials/understanding-the-event-loop-in-javascript--cms-107240
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 execution of code, collection and processing of events. It also handles execution of any queued sub-tasks.

In this tutorial, we will learn the basics of 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 and 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 invocation of functions which will then be placed on the stack. The processing of 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.

The heap is basically a large region of memory where objects are stored and allocated. Its primary purpose is 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 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:

function helloWorld() {
2
    console.log("Hello, World!");
3
}
4
5
function helloPerson(name) {
6
    console.log(`Hello, ${name}!`);
7
}
8
9
function helloTeam() {
    console.log("Hello, Team!");
    helloPerson("Monty");
}
function byeWorld() {
    console.log("Bye, World!");
}
helloWorld();
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 function helloWorld() is invoked and put on the stack. It logs Hello, World! which completes its execution so it is taken off the stack. The function helloTeam() is invoked next and put on the stack. During its execution, we log Hello, Team! and invoke helloPerson(). The execution of helloTeam() isn't still 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 function helloTeam() goes off stack after that and we finally get to byeWorld(). It logs Bye, World! and then goes off stack.

The queue stays empty this whole time.

Now, consider a slight variation of the above code:

function helloWorld() {
2
    console.log("Hello, World!");
3
}
4
5
function helloPerson(name) {
6
    console.log(`Hello, ${name}!`);
7
}
8
9
function helloTeam() {
    console.log("Hello, Team!");
    setTimeout(() => {
        helloPerson("Monty");
    }, 0);
}
function byeWorld() {
    console.log("Bye, World!");
}
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 actually been set to zero. Therefore, we would expect Hello, Monty! to be output before Bye, World!. Why that doesn't happen can be explained 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 your provide to setTimeout() isn't the guaranteed time when the callback would 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 make DOM related changes. Consider the following example where we find the number of primes in a given range:

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
    }
  }
  return true;
}
function listPrimesInRange(start, end) {
  const primes = [];
  for (let num = start; num <= end; num++) {
    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 function listPrimesInRange() 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:

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)) {
      primeNumbers.push(num);
    }
    if (num == next) {
      percentage = ((num - begin) * 100) / (end - begin);
      percentage = Math.floor(percentage);
      progress.innerText = `Progress ${percentage}%`;
      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 on 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 are likely to get a message about the webpage slowing down Firefox and to 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 that it has to do.

Since JavaScript keeps executing a function till completion, doing a lot of computations can sometime "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.


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK