📜 ⬆️ ⬇️

How JS works: event loop, asynchrony, and five ways to improve code with async / await


Here is the fourth part of a series of materials on the internal features of the work of JavaScript. These materials, on the one hand, are aimed at studying the basic elements of the JS language and ecosystem, on the other hand, they provide recommendations based on the software development practice in SessionStack . A competitive JS application should be fast and reliable. The creation of such applications is the goal to which, ultimately, anyone who is interested in the mechanisms of JavaScript aims.

image

This article can be considered the development of the first material. The limitations of the single-threaded execution model will be discussed here, and we will talk about how to overcome these limitations. For example - to develop high-quality user interfaces on JS. As usual, at the end of the material practical recommendations on the use of the considered technologies will be given. Since the main topic of this article is asynchronous development, there will be five tips for improving code using async / await .

Restrictions of single-threaded code execution model


In the first article, we reflected on the following question: “What happens when there is a function in the call stack that needs a lot of time to execute?”. We continue these reflections.
')
Imagine a complex image processing algorithm, the implementation of which runs in a browser. When there is a working function in the call stack, the browser can do nothing more. It is locked. This means that the browser cannot display anything on the screen, it cannot execute other code. The most noticeable consequence of this situation is the “brake” of the user interface. The webpage is simply "stuck."

In some cases, this may not be such a serious problem. However, this is not the worst. As soon as the browser starts to perform too many tasks, it may not react for quite a long time to the user's actions. Usually in such a situation, browsers take protective measures and give an error message, asking the user whether to close the problem page. It would be better for the user not to see such messages. They completely destroy all the efforts of developers to create beautiful and user-friendly interfaces.


What if you want a web application to look good and perform complex calculations? We begin the search for an answer to this question with an analysis of the building blocks of JS applications.

Building blocks of JavaScript programs


JavaScript application code can be placed in a single .js file, but it will almost certainly consist of several blocks. At the same time, only one of these blocks will be executed at a certain moment in time, say - right now. The rest will be executed later. The most common blocks of code in JavaScript are functions.

Apparently, most new JS developers are not fully aware of the fact that "later" does not necessarily immediately follow "now." In other words, a task that cannot be performed "now" must be performed asynchronously. This means that we will not encounter blocking behavior that you, perhaps unconsciously, could expect.

Take a look at the following example:

 // ajax(..) -   Ajax- var response = ajax('https://example.com/api'); console.log(response); //   response     api 

You may be aware that standard Ajax requests are not executed synchronously. This means that the ajax(..) function, immediately after its call, cannot return some value that could be assigned to the response variable.

A simple mechanism for organizing the "waiting" result returned by an asynchronous function is the use of so-called callback functions, or callbacks:

 ajax('https://example.com/api', function(response) {   console.log(response); //   response   api }); 

Here I would like to note that you can execute an Ajax request synchronously. However, this should not be done. If you perform a synchronous Ajax request, the user interface of the JS application will be blocked. The user will not be able to click on the button, enter data in the field, he will not even be able to scroll the page. Synchronous execution of Ajax requests will not allow the user to interact with the application. Such an approach, although possible, leads to disastrous consequences.

This is how this horror looks, however, please never write anything like this, do not turn the web into a place where it is impossible to work normally:

 //   ,    jQuery jQuery.ajax({   url: 'https://api.example.com/endpoint',   success: function(response) {       //  - .   },   async: false //    - ,      }); 

Here we gave examples on Ajax requests, however, any piece of code can be run asynchronously.

For example, you can do this using the setTimeout(callback, milliseconds) function. It allows you to schedule the execution of events (setting a time-out) that should occur after the moment when the function is called. Consider an example:

 function first() {   console.log('first'); } function second() {   console.log('second'); } function third() {   console.log('third'); } first(); setTimeout(second, 1000); //   second  1000  third(); 

This is what this code will bring to the console:

 first third second 

Study of the cycle of events


This may seem strange, but prior to ES6 JavaScript, although it allowed you to make asynchronous calls (like setTimeout described above), it did not contain any built-in asynchronous programming mechanisms. JS engines were only doing single-threaded execution of certain code fragments, one at a time.

To learn more about how JavaScript engines (and, in particular, V8) work, take a look at this material .

So, who tells the JS engine that he has to execute a certain fragment of the program? In reality, the engine does not work in isolation - its own code is executed inside some environment, which, for most developers, is either a browser or Node.js. In fact, these days there are JS engines for various types of devices - from robots to smart light bulbs. Each such device represents its own version of the environment for the JS-engine.

A common characteristic of all such environments is the built-in mechanism, which is called the event loop. It supports the execution of program fragments, invoking the JS engine for this.

This means that the engine can be considered as the runtime environment for any JS code called on demand. And event planning (that is, JS code execution sessions) is handled by environmental mechanisms external to the engine.

So, for example, when your program performs an Ajax request to load some data from the server, you write a command to write this data to the response variable inside the callback, and the JS engine tells the environment: “Listen, I'm going to pause the program, but when you finish this network request and get some data, please call this callback. ”

The browser then installs a listener waiting for a response from the network service, and when it has something to return to the program that made the request, it schedules a callback call, adding it to the event loop.

Take a look at the following diagram:

Details about the heap (memory heap) and the call stack can be found here . And what are the Web API? In general, these are streams to which we do not have direct access; we can only perform calls to them. They are built into the browser, where asynchronous actions are performed.

If you are developing under Node.js, then similar APIs are implemented using C ++ tools.

So what is the event loop?


The event loop solves one main task: it watches the call stack and the callback queue. If the call stack is empty, the loop takes the first event from the queue and pushes it onto the stack, which causes the event to run.

Such an iteration is called a tick of the event loop. Every event is just a callback.

Consider the following example:

 console.log('Hi'); setTimeout(function cb1() {   console.log('cb1'); }, 5000); console.log('Bye'); 

Let's take a step by step execution of this code and see what happens in the system.

1. Nothing happens yet. The browser console is clean, the call stack is empty.


2. The console.log('Hi') command is added to the call stack.


3. The console.log('Hi') command is executed.


4. The console.log('Hi') command is removed from the call stack.


5. The setTimeout(function cb1() { ... }) command is added to the call stack.


6. The setTimeout(function cb1() { ... }) command is executed. The browser creates a timer that is part of the Web API. It will perform a countdown.


7. The setTimeout(function cb1() { ... }) command has completed and is removed from the call stack.


8. The console.log('Bye') command is added to the call stack.


9. The console.log('Bye') command is executed.


10. The command console.log('Bye') is removed from the call stack.


11. After at least 5000 ms have cb1 , the timer ends and puts the cb1 cb1 on the callback queue.


12. The event loop takes the cb1 function from the cb1 queue and pushes it onto the call stack.


13. The cb1 function cb1 executed and adds console.log('cb1') to the call stack.


14. The command console.log('cb1') is executed.


15. The console.log('cb1') command console.log('cb1') is removed from the call stack.


16. The function cb1 is removed from the call stack.


Here, for fixing, the same in animated form.


It is interesting to note that the ES6 specification defines how the event loop should work, namely, it indicates that it is technically within the responsibility of the JS engine, which is beginning to play a more important role in the JS ecosystem. The main reason for this is that promises appeared in ES6 and they need a reliable mechanism for scheduling operations in the event loop queue.

How setTimeout (...) works


Calling setTimeout(…) does not automatically place a callback in the event loop queue. This command starts the timer. When the timer is triggered, the environment puts the callback in the event loop, as a result, during some of the future ticks, this callback will be taken into work and executed. Take a look at this code snippet:

 setTimeout(myCallback, 1000); 

Running this command does not mean that myCallback will be executed in 1000 ms. It would be more correct to say that in 1000 ms. myCallback will be added to the queue. In the queue, however, there may be other events added there earlier, as a result, our callback will have to wait.

There are quite a few articles intended for those who are just starting to engage in asynchronous programming in JavaScript. You can find recommendations for using the setTimeout(callback, 0) command in them. Now you know how the event loop works and what happens when you call setTimeout . Given this, it is quite obvious that a call to setTimeout with the second argument equal to 0 simply postpones the callback call until the call stack is cleared.

Take a look at the following example:

 console.log('Hi'); setTimeout(function() {   console.log('callback'); }, 0); console.log('Bye'); 

Although the time for which the timer is set is 0 ms., The following will be displayed in the console:

 Hi Bye callback 

ES6 tasks


ES6 has a new concept called Job Queue. This design can be considered a layer located on top of the event loop queue. It is quite possible that you came across it when you had to deal with the peculiarities of the asynchronous behavior of promises.

We will now describe this in a nutshell; as a result, when we talk about asynchronous development using promises, you will understand how asynchronous actions are planned and processed.

Imagine this: a job queue is a queue that is attached to the end of each tick in the event loop queue. Some asynchronous actions that may occur during a tick of an event cycle will not cause a new event to be added to the queue of the event loop, but instead an item (that is, a task) will be added to the end of the current tick's job queue. This means that by adding commands to the queue that must be executed in the future, you can be sure of the order in which they will be executed.

Running a job can add additional jobs to the end of the same queue. In theory, it is possible that the “cyclical” task (the task that deals with the addition of other tasks) worked endlessly, depleting the program resources necessary for the transition to the next tick of the cycle of events. Conceptually, this would be like creating an infinite loop like while(true) .

Tasks are something like “hack” s etTimeout(callback, 0) , but implemented in such a way that they allow the observance of a sequence of operations that are performed later, but as soon as possible.

Callbacks


As you already know, callbacks are the most common means of expressing and performing asynchronous actions in JavaScript programs. Moreover, a callback is the most fundamental asynchronous language pattern. Countless JS-applications, even very clever and complex, based solely on callbacks.

All this is good, but callbacks are not perfect. Therefore, many developers are trying to find more successful asynchronous development patterns. It is impossible, however, to effectively use any abstraction, not understanding how everything works, as they say, under the hood.

Below we will look at a couple of such abstractions in detail in order to show why more advanced asynchronous templates, which we will talk about later, are necessary and even recommended for use.

Nested callbacks


Take a look at the following code:

 listen('click', function (e){   setTimeout(function(){       ajax('https://api.example.com/endpoint', function (text){           if (text == "hello") {       doSomething();   }   else if (text == "world") {       doSomethingElse();           }       });   }, 500); }); 

Here there is a chain of three functions nested in each other, each of them is a step in a sequence of actions performed asynchronously.

This code is often called callback hell. But the “hell” is not in the fact that the functions are nested, and not in the fact that the code blocks have to be aligned relative to each other. This is a much deeper problem.

Let's sort this code. First, we wait for the click event, then wait for the timer to trigger, and finally, wait for the arrival of an Ajax response, after which it can all happen again.

At first glance it might seem that this code expresses its asynchronous nature quite naturally, in the form of successive steps. Here is the first step:

 listen('click', function (e) { // .. }); 

Here is the second:

 setTimeout(function(){   // .. }, 500); 

Here is the third:

 ajax('https://api.example.com/endpoint', function (text){   // .. }); 

And finally, this is what happens:

 if (text == "hello") {   doSomething(); } else if (text == "world") {   doSomethingElse(); } 

So, a similar approach to writing asynchronous code looks much more natural, right? There must be a way to write it that way.

Promises


Take a look at the following code snippet:

 var x = 1; var y = 2; console.log(x + y); 

It's all very simple: the values ​​of the variables x and y added and displayed in the console. But what if the value of x or y not available and it has yet to be set? Let's say we need to get from the server what will be written in x and y , and then use this data in the expression. Imagine that we have the functions loadX and loadY , which, respectively, load x and y values ​​from the server. Then imagine that there is a sum function that adds the x and y values ​​as they are loaded.

It all may look like this (it’s scary, right?):

 function sum(getX, getY, callback) {   var x, y;   getX(function(result) {       x = result;       if (y !== undefined) {           callback(x + y);       }   });   getY(function(result) {       y = result;       if (x !== undefined) {           callback(x + y);       }   }); } //    ,   `x` function fetchX() {   // .. } //    ,    `y` function fetchY() {   // .. } sum(fetchX, fetchY, function(result) {   console.log(result); }); 

There is one very important thing. Namely, in this code we treat x and y as values ​​that will be received in the future, and we describe the operation sum(…) (when called without going into the implementation details) as if it doesn't matter to execute , whether or not the x and y values ​​are available at the time of its call.

Of course, the rough approach presented here based on callbacks leaves much to be desired. This is only the first small step towards understanding the advantages, which makes it possible to operate with “future values”, without worrying about the specific time of their appearance.

Promise value


First, take a look at how you can express the x + y operation using promises:

 function sum(xPromise, yPromise) { // `Promise.all([ .. ])`   , //    ,   //   ,     return Promise.all([xPromise, yPromise]) //     ,  //   `X`  `Y`   . .then(function(values){ // `values` -      //   return values[0] + values[1]; } ); } // `fetchX()`  `fetchY()`    //  .     //  **  **. sum(fetchX(), fetchY()) //      //  . //     `then(...)`    //    . .then(function(sum){   console.log(sum); }); 

In this example, there are two layers of promises.

The calls fetchX() and fetchY() are executed directly, and the values ​​they return (promises!) Are passed to sum(...) . Those values ​​that represent these promises may be ready for further use now or later, but each promise behaves in such a way that the moment of accessibility of values ​​is not important in itself. As a result, we reason about the values ​​of x and y without reference to time. These are future values.

The second layer of promises is a promise that creates and returns (using Promise.all([ ... ]) ) the sum(…) call sum(…) . We expect the value that this promise will return, causing then(…) . When the operation sum(…) completed, our future value of the amount is ready and we can display it on the screen. We hide the logic of waiting for future x and y values ​​inside sum(…) .

, sum(…) Promise.all([ … ]) ( promiseX promiseY ). .then(…) , values[0] + values[1] ( , ). , then(…) , , , sum(…) , , , Promise.all([ ... ]) . , then(…) , , , , . , .then(…) .

then(…) , , . — , . — , :

 sum(fetchX(), fetchY()) .then(   //       function(sum) {       console.log( sum );   },   //      function(err) {  console.error( err ); // -    } ); 

x y - , , , s um(…) , . , then(…) , . .

, , — , , , , . , , , .

, , . () , , .

, :

 function delay(time) {   return new Promise(function(resolve, reject){       setTimeout(resolve, time);   }); } delay(1000) .then(function(){   console.log("after 1000ms");   return delay(2000); }) .then(function(){   console.log("after another 2000ms"); }) .then(function(){   console.log("step 4 (next Job)");   return delay(5000); }) // ... 

delay(2000) , 2000 ., then(…) , , then(…) 2000 .

, , , , , , . , , . . , , , , , .

?


, , , Promise . , , , .

, new Promise(…) , , , p instanceof Promise . However, this is not quite true.

, ( , ), Promise , , . , , .

, - , , ES6. , , , , .


, TypeError ReferenceError , .

For example:

 var p = new Promise(function(resolve, reject){   foo.bar(); // `foo`  ,   !   resolve(374);  //      :( }); p.then(   function fulfilled(){       //      :(   },   function rejected(err){       // `err`    `TypeError`       //   `foo.bar()`.   } ); 

, , JS- ( , then(…) )? , , , , :

 var p = new Promise( function(resolve,reject){ resolve(374); }); p.then(function fulfilled(message){   foo.bar();   console.log(message);   //      },   function rejected(err){       //     } ); 

, , foo.bar() «». , . , , . , p.then(…) , TypeError .


, .
, , done(…) , , «». done(…) , , done(…) , , , , .

, , , : done(…) ( ):

 var p = Promise.resolve(374); p.then(function fulfilled(msg){   //     ,   //        console.log(msg.toLowerCase()); }) .done(null, function() {   //    ,       }); 

ES8: async / await


JavaScript ES8 async / await , . , async / await , .

, async . AsyncFunction . , , .

, Promise . , Promise , , . , async , , .

, async , await , , . async- , , , .

Promise JavaScript Future Java C#.

async / await , .
:

 //  -   JS- function getNumber1() {   return Promise.resolve('374'); } //      ,   getNumber1 async function getNumber2() {   return 374; } 

, , , :

 function f1() {   return Promise.reject('Some error'); } async function f2() {   throw 'Some error'; } 

await , async . . async-, then :

 async function loadData() {   // `rp`-   request-promise.   var promise1 = rp('https://api.example.com/endpoint1');   var promise2 = rp('https://api.example.com/endpoint2');    //         //      .   var response1 = await promise1;   var response2 = await promise2;   return response1 + ' ' + response2; } //       ,     `async` //    `then`    Promise loadData().then(() => console.log('Done')); 

« », function . , , , , . IIFE (Immediately Invoked Function Expression, ), .

It looks like this:

 var loadData = async function() {   // `rp`-   request-promise.   var promise1 = rp('https://api.example.com/endpoint1');   var promise2 = rp('https://api.example.com/endpoint2');    //         //      .   var response1 = await promise1;   var response2 = await promise2;   return response1 + ' ' + response2; } 

, async / await .



, , — Babel TypeScript .

async / await , , .

5 ,


â–Ť


async / await . , async / await , . — .then() , , - , , .

, :

 / `rp`-   request-promise. rp('https://api.example.com/endpoint1').then(function(data) { // … }); 

, async / await :

 // `rp`-   request-promise. var response = await rp('https://api.example.com/endpoint1'); 

â–Ť


async / await . , try / catch .

, . , .catch() , try / catch :

 function loadData() {   try { //   .       getJSON().then(function(response) {           var parsed = JSON.parse(response);           console.log(parsed);       }).catch(function(e) { //              console.log(e);       });   } catch(e) {       console.log(e);   } } 

async / await :

 async function loadData() {   try {       var data = JSON.parse(await getJSON());       console.log(data);   } catch(e) {       console.log(e);   } } 

â–Ť


async / await , . — , :

 function loadData() { return getJSON()   .then(function(response) {     if (response.needsAnotherRequest) {       return makeAnotherRequest(response)         .then(function(anotherResponse) {           console.log(anotherResponse)           return anotherResponse         })     } else {       console.log(response)       return response     }   }) } 

— async / await :

 async function loadData() { var response = await getJSON(); if (response.needsAnotherRequest) {   var anotherResponse = await makeAnotherRequest(response);   console.log(anotherResponse)   return anotherResponse } else {   console.log(response);   return response;    } } 

â–Ť


async / await , , , , . :

 function loadData() { return callAPromise()   .then(callback1)   .then(callback2)   .then(callback3)   .then(() => {     throw new Error("boom");   }) } loadData() .catch(function(e) {   console.log(err); // Error: boom at callAPromise.then.then.then.then (index.js:8:13) }); 

— , async / await :

 async function loadData() { await callAPromise1() await callAPromise2() await callAPromise3() await callAPromise4() await callAPromise5() throw new Error("boom"); } loadData() .catch(function(e) {   console.log(err);   //    // Error: boom at loadData (index.js:7:9) }); 

â–Ť


, , — . , .then «step-over», .then , «» . async / await , await , — .

Results


, . , SessionStack , -. , DOM, , JavaScript, , , .

- . , , , , . . , , , .

, . JavaScript, , . , , , .

Dear readers! JS-. , - . — async / await . , , , . ( ) a sync / await .

Source: https://habr.com/ru/post/340508/


All Articles