DEV Community

cclintris
cclintris

Posted on

JavaScript: Event Loop

Intro

In this article, I'm going to talk about a very important javascript concept: the event loop. It is one of the most fundamental and vital part when learning about javascript, helps to understand this programming language to a deeper level, and sometimes it's especially critical when dealing with some bugs. So let's get to it, event loops!

Call Stack and Single Thread

Before getting into event loops, there are some basic things we should get a head start. The first one is the idea of call stacks and single thread processing.

JavaScript is a single threaded language, which we all know, but what exactly does this mean? Well, it means that javascript can only do one single task at a time, can only process one module of code at a time, meaning that javascript processes code line by line, one line at a time.

Call Stacks record where our code is processed to. For example, if we process a function, we'll push this function to the top of the call stack, and when done processing, this function would be popped out of the stack.

For example:

function a() {
  b();
}

function b() {
  console.log("hi");
}

a();
Enter fullscreen mode Exit fullscreen mode

For the code above, the call stack would like this:

Image description

Idea behind asynchronous execution

Single thread JavaScript

So now we know javascript is a single thread language. It is primarily used to interact with users and to control DOM elements.

Javascript also has the concept of asynchronous and synchronous. With this mechanism, it resolves the problem of blocking. Here, we give a simple explanation between these two mechanisms.

  • synchronous

If when a function returns, the caller is able to get the expected result, then this function is a synchronous function.

  • asynchronous

If when a function returns, the caller is unable to get the expected result immediately, instead, the caller needs to use some kind of way to callback this expected result at some point in the future, then this function is a asynchronous function.

Multi thread Browser

Now we know javascript is single threaded, meaning js can only do one task at a time. So how, why are browsers able to process asynchronous tasks simultaneously.

This is because browsers are multi threaded. When js needs to process asynchronous tasks, browsers are going to activate another thread in service of these asynchronous tasks. Put it in a more simple way, when we say JavaScript is single threaded, it means that there is only one single thread actually processing the js code, which is the engine that browsers provide for js(primary thread). Besides the primary thread for processing js code, there are lots of other threads which are not mainly in usage of running js code.

For example, if there is a request to send data in the main thread, the browser will distribute this task to the Http request thread, then proceed doing other tasks, and when the data is successfully fetched, it will then continue to the callback js code where it left, and then distribute the callback tasks to the primary thread to process js code.

In other words, when you write js code to send data requests no matter in any protocols, you think you are the one sending the request, however, it is actually the browser that is the one sending the request. For Http request as an instance, it is actually the http request thread of the browser that sends the request. Javascript code is just responsible for the callback process.

To conclude shortly, when we say a js asynchronous task, to be frank, the asynchronous ability isn't an inherent feature of javascript, it is actually the ability that browsers provide.

Image description

As we see a modern architecture of browsers, there are more than one renderers, and more of them are uncharted in this pic.

Event loops for Browsers

JavaScript classifies its tasks into two catagories: synchronous and asynchronous tasks.

  • synchronous tasks: For tasks queued for execution on the main thread, only when one task has been completely executed can the next task be executed.

  • asynchronous tasks: Instead of entering the main thread, it is placed in the task queue. If there are multiple asynchronous tasks, they need to wait in the task queue. The task queue is similar to a buffer. The next task will be moved to the execution stack and the main thread will execute the task on the execution stack.

Well, mentioning the task queue and execution stack, we have to first explain what these are.

execution stack and task queue

  • execution stack:

As can be seen from the name, it is a stack data structure that stores function calls, following the principle of first-in, last-out(FILO). It is mainly responsible for keeping track of all the code being executed. Whenever a function is executed, the function is popped from the stack; if there is code that needs to be executed, a push operation is performed. It kinda works like the call stack previously mentioned above.

  • task queue:

Again, as can be seen from the name, the task queue uses the queue data structure, which is used to store asynchronous tasks and follows the principle of first-in, first-out(FIFO). It is mainly responsible for sending new tasks to the queue for processing.

When JavaScript executes code, it arranges the synchronized code in the execution stack in order, and then executes the functions within in order. When an asynchronous task is encountered, it is put into the task queue, and after all the synchronous codes of the current execution stack are executed, the callback of the completed asynchronous task will be removed from the task queue and put into the execution stack. It works just like a loop and so on and so one, until all tasks are executed.

Image description

In an event-driven mode which applies to javascript, at least one execution loop is included to check for new tasks in the task queue. By looping continuously, the callback, or say more plainly, the results, of the asynchronous task is taken out to the main thread for execution.

This whole process is called the event loop.

Macro and Micro tasks

In fact, there is more than one task queue. According to different types of tasks, it can be divided into micro task queue and macro task queue. Here, we'll list some of the most common tasks that you may ecounter, forming a more clear understanding of the difference between micro and macro tasks.

  • Macro tasks: script.js(overall code), setTimeout, setInterval, I/O, UI interaction events, setImmediate (Node.js environment)

  • Micro tasks: Promise, MutaionObserver, process.nextTick (Node.js environment)

Tasks in task queues are executed as in the picture below:

Image description

It can be seen that the execution of Event loops when processing the macro-tasks and micro-tasks is as:

  1. The JavaScript engine first takes the first task from the macro task queue;

  2. After the execution is completed, take out all the tasks in the micro-tasks and execute them in sequence (this includes not only the first micro-tasks in the queue at the beginning of execution).If new micro-tasks are generated during this step, they also need to be executed. That is to say, the new micro-tasks generated during the execution of micro-tasks will not be postponed to the next cycle for execution, but will continue to be executed in the current cycle.

  3. Then take the next task from the macro task queue. After the execution is completed, take out all the tasks in micro task queues again, and the cycle repeats until the tasks in the two queues are all taken out.

So to conclude, an Eventloop cycle will process one macro-task and all the micro-tasks generated in this loop.
Enter fullscreen mode Exit fullscreen mode

Let's take a look at the example below:

console.log("sync1");

setTimeout(() => {
  console.log("setTimeout");
}, 0);

new Promise((resolve) => {
  console.log("sync2");
  resolve();
}).then(() => {
  console.log("promise.then");
});

console.log("sync3");
Enter fullscreen mode Exit fullscreen mode

The output should be:

"sync1";
"sync2";
"sync3";
"promise.then";
"setTimeout";
Enter fullscreen mode Exit fullscreen mode

Well, if you're answer is not exactly same as the output shown above, don't worry, let's dig in and see how does this piece of code process exactly.

  1. When the first console is encountered, it is a synchronous code, which is added to the execution stack, executed and popped from the stack, and sync1 is printed out

  2. When setTimeout is encountered, it is a macro task and is added to the macro task queue

  3. When encountering the console in new Promise, because it is resolved immediately, it is a synchronous code, which is added to the execution stack, executed and popped from the stack, and sync2 is printed out

  4. When encountering Promise then, it is a microtask and added to the microtask queue

  5. When the third console is encountered, it is a synchronous code, which is added to the execution stack, executed and popped from the stack, and sync3 is printed out

  6. At this point, the execution stack is empty, so execute all tasks in the microtask queue, and print out promise.then

  7. After executing the tasks in the micro-task queue, execute one task in the macro-task queue and print out setTimeout

  8. At this point, both the macro-task queue and micro-task queue is empty, end of execution

For step 6 and 7, you might be confused, that why shouldn't setTimeout print before promise.then, as when done executing console.log("sync3");, it should look back at the macro-task queue first since execution stack is empty, then execute all tasks in micro-tasks.

Well, the tricky part lies in the script macro-task. Notice that the whole javascript code, as in script, is a macro-task. Moreover, it is always the first macro-task that will be added to the macro-task queue and the first to be executed.

I'm sure everything is clear now. So actually, after executing console.log("sync3");, it indicates that the first macro-task is completed. Thus, it will continue the first round of Eventloop by looking into the micro-task queue, seeing Promise.then, execute it, and boom! This is when the the first round of Eventloop actually stops. The the second round of Eventloop then starts again, and so on...

From the workflow of macrotasks and microtasks above, the following conclusions can be drawn:

  • Micro-tasks and macro-tasks are bound, and each macro-task will create its own micro-task queue when executed.

  • The execution duration of the microtask will affect the duration of the current macrotask. For example, during the execution of a macro-task, 10 micro-tasks are generated, and the time to execute each micro-task is 10ms, then the time to execute these 10 micro-tasks is 100ms. It can also be said that these 10 micro-tasks caused a 100ms delay for the macro-task.

  • There is only one macro-task queue, and each macro-task has its own micro-task queue.So each round of the Eventloop consists of one macro-task + multiple micro-tasks.

  • A very important point is to always remember that the first task in the macro-task queue will always be the overall script code.

Below is a question also about the output of event loops, a bit more complicated. Maybe it's time for you to try it yourself!

setTimeout(function () {
  console.log(" set1");
  new Promise(function (resolve) {
    resolve();
  }).then(function () {
    new Promise(function (resolve) {
      resolve();
    }).then(function () {
      console.log("then4");
    });
    console.log("then2 ");
  });
});

new Promise(function (resolve) {
  console.log("pr1");
  resolve();
}).then(function () {
  console.log("then1");
});

setTimeout(function () {
  console.log("set2");
});

console.log(2);

new Promise(function (resolve) {
  resolve();
}).then(function () {
  console.log("then3");
});
Enter fullscreen mode Exit fullscreen mode

Your answer should be like:

pr1
2
then1
then3
set1
then2
then4
set2
Enter fullscreen mode Exit fullscreen mode

Top comments (0)