Understanding asynchronous JavaScript [Translated by Sukhjinder Arora]

https://blog.bitsrc.io/understanding-asynchronous-javascript-the-event-loop-74cd408419ff
  • Transfer
Hi, Habr! I present to you the translation of the article "Understanding Asynchronous JavaScript" by Sukhjinder Arora.



From the author of translation: I hope the translation of this article will help you to get acquainted with something new and useful. If the article helped you, do not be lazy and thank the author of the original. I do not pretend to be a professional translator, I am just starting to translate articles and will be happy with any meaningful feedback.

JavaScript is a single-threaded programming language in which only one thing at a time can be executed. That is, in one stream, the JavaScript engine can process only 1 operator at a time.

Although single-threaded languages ​​simplify coding, since you can not worry about concurrency issues, it also means that you cannot perform long operations such as accessing the network without blocking the main stream.

Submit an API request to get some data. Depending on the situation, the server may take some time to process your request, and the execution of the main stream will be blocked, which will cause your web page to stop responding to requests for it.

This is where the asynchronous JavaScript comes into play. Using JavaScript asynchrony (callbacks, promises, and async / await) you can perform long network requests without blocking the main thread.

Although it is not necessary to study all these concepts in order to be a good JavaScript developer, it is helpful to know them.

So, without further ado, let's begin.

How does synchronous javascript work?


Before we delve into the work of asynchronous JavaScript, let's first understand how to run synchronous code inside the JavaScript engine. For example:

const second = () => {
  console.log('Hello there!');
}
const first = () => {
  console.log('Hi there!');
  second();
  console.log('The End');
}
first();

In order to understand how the code above is executed inside the JavaScript engine, we need to understand the concept of the execution context and the call stack (also known as the execution stack).

Execution context


The execution context is an abstract concept of the environment in which code is evaluated and executed. Whenever any code is executed in JavaScript, it is run in the execution context.

The function code is executed inside the function execution context, and the global code, in turn, is executed within the global execution context. Each function has its own execution context.

Call stack


Call stack refers to a stack with a LIFO structure (Last in, First Out, Last In, First Out), which is used to store all execution contexts created during the execution of the code.

There is only one call stack in JavaScript, since it is a single-threaded programming language. The LIFO structure means that items can be added and removed only from the top of the stack.

Let's now go back to the code snippet above and try to understand how the JavaScript engine performs it.

const second = () => {
  console.log('Hello there!');
}
const first = () => {
  console.log('Hi there!');
  second();
  console.log('The End');
}
first();



And so, what happened here?


When the code started to run, a global execution context was created (represented as main () ) and added to the top of the call stack. When a call to the first () function is encountered , it is also added to the top of the stack.

Next, console.log ('Hi there!') Is placed at the top of the call stack , after execution it is removed from the stack. After that we call the second () function , so it is placed on top of the stack.

console.log ('Hello there!') is added to the top of the stack and is removed from it upon completion. The second () function is complete; it is also removed from the stack.

console.log ('The End')added to the top of the stack and deleted upon completion. After that, the first () function ends and is also removed from the stack.

Program execution ends, so the global calling context ( main () ) is removed from the stack.

How does asynchronous javascript work?


Now that we have a general understanding of the call stack and how synchronous JavaScript works, let's go back to asynchronous JavaScript.

What is blocking?


Let's assume that we perform image processing or a network request synchronously. For example:

const processImage = (image) => {
  /**
  * Выполняем обработку изображения
  **/console.log('Image processed');
}
const networkRequest = (url) => {
  /**
  * Обращаемся к некоторому сетевому ресурсу
  **/return someData;
}
const greeting = () => {
  console.log('Hello World');
}
processImage(logo.jpg);
networkRequest('www.somerandomurl.com');
greeting();

Image processing and network request takes time. When the processImage () function is called, its execution will take some time, depending on the size of the image.

When the processImage () function is executed, it is removed from the stack. After it, the networkRequest () function is called and added to the stack . This will again take some time before completing the execution.

Finally, when the networkRequest () function is executed, the greeting () function is called , since it only contains the console.log method , and this method is usually executed quickly, the greeting () function will execute and end instantly.

As you can see, we need to wait until the function (such as processImage () or networkRequest () ) is completed. This means that such functions block the call stack or main thread. As a result, we cannot perform other operations until the code above is executed.

So what is the solution?


The simplest solution is asynchronous callback functions. We use them to make our code non-blocking. For example:

const networkRequest = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
};
console.log('Hello World');
networkRequest();

Here I used the setTimeout method to simulate a network request. Please remember that setTimeout is not part of the JavaScript engine, it is part of the so-called web API (in the browser) and C / C ++ APIs (in node.js).

In order to understand how this code is executed, we need to deal with a few more concepts, such as an event loop and a callback queue (also known as a task queue or message queue).



The event loop, the web API and the message queue / task queue are not part of the JavaScript engine, it is part of the browser-based JavaScript runtime or the JavaScript runtime in Nodejs (in the case of Nodejs). In Nodejs, the web APIs are replaced with C / C ++ APIs.

Now let's go back to the code above, and see what happens in the case of asynchronous execution.

const networkRequest = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
};
console.log('Hello World');
networkRequest();
console.log('The End');



When the code above is loaded into the browser console.log ('Hello World') is added to the stack and removed from it upon completion. Next, a call to the networkRequest () function is encountered , it is added to the top of the stack.

The next function is called setTimeout () and placed on top of the stack. The setTimeout () function has 2 arguments: 1) a callback function and 2) time in milliseconds.

setTimeout () starts a timer for 2 seconds in the web API environment. At this stage, setTimeout () is completed and removed from the stack. After that, console.log ('The End') is added to the stack , executed and removed from it upon completion.

Meanwhile, the timer has expired, now the callback is added to the message queue. But the callback cannot be executed immediately, and it is here that the event loop enters the process.

Event loop


The task of the event loop is to monitor the call stack and determine whether it is empty or not. If the call stack is empty, the event loop looks into the message queue to see if there are callbacks that are waiting to be executed.

In our case, the message queue contains one callback, and the execution stack is empty. Therefore, the event loop adds a callback to the top of the stack.

After console.log ('Async Code') is added to the top of the stack, executed and removed from it. At this point, the callback is made and removed from the stack, and the program is completely completed.

DOM events


The message queue also contains callbacks from DOM events, such as clicks and “keyboard” events. For example:

document.querySelector('.btn').addEventListener('click',(event) => {
  console.log('Button Clicked');
});

In the case of DOM events, the event handler is in the web API environment, waiting for a specific event (in this case, a click), and when this event occurs, the callback function is placed in the message queue, waiting for its execution.

We learned how asynchronous callbacks and DOM events are performed that use a message queue to store callbacks waiting to be executed.

ES6 Microtouch Queue


Note author of the translation: In the article, the author used the message / task queue and the job / micro-taks queue, but if you translate the task queue and the job queue, then in theory it turns out the same thing. I talked to the author of the translation and decided to simply omit the notion of job queue. If you have any thoughts on this subject, then I’m waiting for you in the comments

Link to the translation of the article on promises from the same author


ES6 introduced the concept of a queue of microtasks, which are used by “promises” in JavaScript. The difference between the message queue and the microtask queue is that the microtouch queue has a higher priority than the message queue, which means that promises within the microtouch queue will be executed earlier than callbacks in the message queue.

For example:

console.log('Script start');
setTimeout(() => {
  console.log('setTimeout');
}, 0);
newPromise((resolve, reject) => {
    resolve('Promise resolved');
  }).then(res =>console.log(res))
    .catch(err =>console.log(err));
console.log('Script End');

Conclusion:

Script start
Script End
Promise resolved
setTimeout

As you can see, the “promise” was completed before setTimeout , all because the promise response is stored within the queue of microtasks, which has a higher priority than the message queue.

Let's look at the following example, this time 2 “promises” and 2 setTimeout :

console.log('Script start');
setTimeout(() => {
  console.log('setTimeout 1');
}, 0);
setTimeout(() => {
  console.log('setTimeout 2');
}, 0);
newPromise((resolve, reject) => {
    resolve('Promise 1 resolved');
  }).then(res =>console.log(res))
    .catch(err =>console.log(err));
newPromise((resolve, reject) => {
    resolve('Promise 2 resolved');
  }).then(res =>console.log(res))
    .catch(err =>console.log(err));
console.log('Script End');

Conclusion:

Script start
Script End
Promise1 resolved
Promise2 resolved
setTimeout 1
setTimeout 2

And again, both of our “promises” were executed before the callbacks inside setTimeout , since the event loop considers the tasks from the microtasks queue more important than the tasks from the message queue / task queue.

If another “promise” appears during the execution of tasks from the microtwist queue, it will be added to the end of this queue and executed before the callbacks from the message queue, and it doesn’t matter how long they wait for their execution.

For example:

console.log('Script start');
setTimeout(() => {
  console.log('setTimeout');
}, 0);
newPromise((resolve, reject) => {
    resolve('Promise 1 resolved');
  }).then(res =>console.log(res));
newPromise((resolve, reject) => {
  resolve('Promise 2 resolved');
  }).then(res => {
       console.log(res);
       returnnewPromise((resolve, reject) => {
         resolve('Promise 3 resolved');
       })
     }).then(res =>console.log(res));
console.log('Script End');

Conclusion:

Script start
Script End
Promise1 resolved
Promise2 resolved
Promise3 resolved
setTimeout

Thus, all tasks from the microtroke queue will be completed before tasks from the message queue. That is, the event processing cycle will first clear the queue of microtasks, and only after that will it start performing callbacks from the message queue.

Conclusion


So, we learned how asynchronous JavaScript and concepts work: call stack, event loop, message queue / task queue, and microtask queue that make up the JavaScript runtime

Also popular now: