📜 ⬆️ ⬇️

JavaScript: Asynchronous Programming Techniques

Synchronous JavaScript code, the author of which did not seek to confuse those who will read this code, usually looks simple and clear. The commands of which it is composed are executed in the order in which they follow in the text of the program. A bit of confusion can make raising declarations of variables and functions, but to turn this feature of JS into a problem, you have to try very hard. Synchronous JavaScript code has only one serious drawback: one cannot go far on it.



Virtually every useful JS program is written using asynchronous development methods. This is where callbacks come in, in common parlance, callbacks. Here there are “promises” or Promise objects, usually called promises. Here you can encounter generators and async / await constructions. Asynchronous code, in comparison with synchronous, is usually more difficult to write, read and maintain. Sometimes it turns into completely creepy structures like hell callbacks. However, it can not do without.
')
Today we offer to talk about the features of callbacks, promises, generators, and async / await constructions, and to think about how to write simple, understandable and efficient asynchronous code.

About synchronous and asynchronous code


We'll start by looking at fragments of synchronous and asynchronous JS code. For example, the usual synchronous code:

console.log('1') console.log('2') console.log('3') 

He, without any particular difficulty, displays numbers from 1 to 3 to the console.

Now the code is asynchronous:

 console.log('1') setTimeout(function afterTwoSeconds() { console.log('2') }, 2000) console.log('3') 

Here the sequence 1, 3, 2 will be displayed. The number 2 is derived from the callback, which handles the timer triggering event specified when the setTimeout function was called. The callback will be called, in this example, after 2 seconds. The application will not stop while waiting for the two seconds to expire. Instead, its execution will continue, and when the timer fires, the afterTwoSeconds function will be called.

Perhaps, if you are just starting the path of a JS developer, you will be asked questions: “Why is this all? Maybe you can convert asynchronous code to synchronous? ". Let's look for answers to these questions.

Formulation of the problem


Suppose we are faced with the task of finding a GitHub user and loading data about his repositories. The main problem here is that we do not know the exact username, so we need to display all users with names similar to what we are looking for and their repositories.

In terms of the interface, we restrict ourselves to something simple .


Simple search interface for GitHub users and their corresponding repositories

In the examples, requests will be executed using XMLHttpRequest (XHR), but you can easily use jQuery ( $.ajax ) here, or the more modern standard approach, based on the use of the function fetch . Both that and another is reduced to use of promises. The code, depending on the campaign, will change, but here, for a start, this example:

 //  url   -  'https://api.github.com/users/daspinola/repos' function request(url) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) {   if (xhr.readyState === 4) {     if (xhr.status === 200) {      //          } else {      //           }   } } xhr.ontimeout = function () {   //      ,   ,     } xhr.open('get', url, true) xhr.send(); } 

Pay attention to the fact that in these examples it’s important not what comes from the server and how it will be processed, but the organization of the code itself using different approaches that you can use in your asynchronous development.

Callback functions


You can do a lot of things with functions in JS, including passing in arguments to other functions. Usually this is done in order to call the transferred function after the completion of some process, which may take some time. This is a callback function. Here is a simple example:

 //   "doThis"      ,    -   "andThenThis".  "doThis"  ,   ,  ,   ,   "andThenThis". doThis(andThenThis) //  "doThis"         "callback" , ,   ,      function andThenThis() { console.log('and then this') } //  ,      ,   , "callback" -     function doThis(callback) { console.log('this first') //  ,  ,      ,  ,      , '()',     callback() } 

Using this approach to solve our problem, we can write the following request function:

 function request(url, callback) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) {   if (xhr.readyState === 4) {     if (xhr.status === 200) {      callback(null, xhr.response)     } else {      callback(xhr.status, null)     }   } } xhr.ontimeout = function () {  console.log('Timeout') } xhr.open('get', url, true) xhr.send(); } 

Now the function for executing the request accepts the callback parameter, therefore, after executing the request and receiving the server response, the callback will be called both in case of an error and in case of successful completion of the operation.

 const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` request(userGet, function handleUsersList(error, users) { if (error) throw error const list = JSON.parse(users).items list.forEach(function(user) {   request(user.repos_url, function handleReposList(err, repos) {     if (err) throw err     //      }) }) }) 

Let's analyze what is happening here:


Please note that using the error object as the first parameter is a widespread practice, in particular, for developing using Node.js.

If we give our code a more complete look, provide it with error handling tools and separate the definition of callback functions from the request execution code, which improves the readability of the program, we get the following:

 try { request(userGet, handleUsersList) } catch (e) { console.error('Request boom! ', e) } function handleUsersList(error, users) { if (error) throw error const list = JSON.parse(users).items list.forEach(function(user) {   request(user.repos_url, handleReposList) }) } function handleReposList(err, repos) { if (err) throw err //     console.log('My very few repos', repos) } 

This approach works, but using it we run the risk of problems like the race conditions of queries and the complexity of error handling. However, the main trouble with callbacks, which, considering what happens in the forEach loop, here are three, is that such code is hard to read and maintain. A similar problem exists, perhaps, from the day the callback functions appeared, it is widely known as callback hell.


Hell kollbekov in all its glory. The image is taken from here .

In this case, under the “race condition” we understand the situation when we do not control the procedure for obtaining data on user repositories. We request data for all users, and it may well turn out that the answers to these requests will be mixed up. Say, the answer for the tenth user comes first, and the second - the last. Below we talk about a possible solution to this problem.

Promises


Using promises can improve code readability. As a result, for example, if a new developer comes to your project, he will quickly understand how everything is arranged there.

In order to create a promise, you can use this design:

 const myPromise = new Promise(function(resolve, reject) { //    if (codeIsFine) {   resolve('fine') } else {   reject('error') } }) myPromise .then(function whenOk(response) {   console.log(response)   return response }) .catch(function notOk(err) {   console.error(err) }) 

Let's sort this example:


Here is something to remember when working with promises:


Please note that promises can be created without using separately defined functions, describing functions at the moment of creating promises. What is shown in our example is only a common way to initialize promises.

In order not to get bogged down in theory, let us return to our example. Rewrite it using promises.

 function request(url) { return new Promise(function (resolve, reject) {   const xhr = new XMLHttpRequest();   xhr.timeout = 2000;   xhr.onreadystatechange = function(e) {     if (xhr.readyState === 4) {       if (xhr.status === 200) {         resolve(xhr.response)       } else {         reject(xhr.status)       }     }   }   xhr.ontimeout = function () {     reject('timeout')   }   xhr.open('get', url, true)   xhr.send(); }) } 

With this approach, when you call request , the following will be returned.


This is a promise in the waiting state. It can either be resolved successfully or rejected.

Now, using the new request function, we will rewrite the rest of the code.

 const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const myPromise = request(userGet) console.log('will be pending when logged', myPromise) myPromise .then(function handleUsersList(users) {   console.log('when resolve is found it comes here with the response, in this case users ', users)   const list = JSON.parse(users).items   return Promise.all(list.map(function(user) {     return request(user.repos_url)   })) }) .then(function handleReposList(repos) {   console.log('All users repos in an array', repos) }) .catch(function handleErrors(error) {   console.log('when a reject is executed it will come here ignoring the then statement ', error) }) 

Here we find ourselves in the first expression .then with the successful resolution of promise. We have a list of users. In the second expression .then we pass an array with repositories. If something went wrong, we’ll end up in a .catch expression.

Through this approach, we have dealt with the state of the race and with some of the problems that arise. Ada kollbekov here is not observed, but the code is still not read so easy. In fact, our example can be further improved by extracting callback functions from it:

 const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const userRequest = request(userGet) //       ,        userRequest .then(handleUsersList) .then(repoRequest) .then(handleReposList) .catch(handleErrors) function handleUsersList(users) { return JSON.parse(users).items } function repoRequest(users) { return Promise.all(users.map(function(user) {   return request(user.repos_url) })) } function handleReposList(repos) { console.log('All users repos in an array', repos) } function handleErrors(error) { console.error('Something went wrong ', error) } 

With this approach, one look at the names of callbacks in .then expressions reveals the meaning of the call to userRequest . The code is easy to work with and easy to read.

In fact, this is only the tip of the iceberg of what is called the promise. Here is the material that I recommend to read to those who want to more thoroughly immerse themselves in this topic.

Generators


Another approach to the solution of our problem, which, however, you rarely meet, is generators. The topic is a little more complicated than the rest, so if you feel that it’s too early for you to study, you can skip ahead to the next section of this material.

In order to define a generator function, you can use the asterisk, “*”, after the keyword function . Using generators, asynchronous code can be made very similar to synchronous. For example, it may look like this:

 function* foo() { yield 1 const args = yield 2 console.log(args) } var fooIterator = foo() console.log(fooIterator.next().value) //  1 console.log(fooIterator.next().value) //  2 fooIterator.next('aParam') //    console.log      'aParam' 

The point here is that the generators, instead of return , use the expression yield , which stops the execution of the function until the next call to the .next iterator. This is similar to the expression .then in promises, which is performed when promise is resolved.

Let us now see how to apply all this to our task. So, here is the request function:

 function request(url) { return function(callback) {   const xhr = new XMLHttpRequest();   xhr.onreadystatechange = function(e) {     if (xhr.readyState === 4) {       if (xhr.status === 200) {         callback(null, xhr.response)       } else {         callback(xhr.status, null)       }     }   }   xhr.ontimeout = function () {     console.log('timeout')   }   xhr.open('get', url, true)   xhr.send() } } 

Here, as usual, we use the url argument, but instead of immediately executing the request, we want to execute it only when we have a callback function to process the response.

The generator will look like this:

 function* list() { const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const users = yield request(userGet) yield for (let i = 0; i<=users.length; i++) {   yield request(users[i].repos_url) } } 

This is what happens here:


Using this all will look like this:

 try { const iterator = list() iterator.next().value(function handleUsersList(err, users) {   if (err) throw err   const list = JSON.parse(users).items     //       iterator.next(list)     list.forEach(function(user) {     iterator.next().value(function userRepos(error, repos) {       if (error) throw repos       //              console.log(user, JSON.parse(repos))     })   }) }) } catch (e) { console.error(e) } 

Here we can individually process the list of repositories of each user. In order to improve this code, we could select callback functions, as we have done above.

I am ambiguous about generators. On the one hand, you can quickly understand what to expect from the code by looking at the generator, on the other hand, the execution of generators leads to problems similar to those that arise in the hell of callbacks.

It should be noted that generators are a relatively new opportunity, as a result, if you expect to use your code in older versions of browsers, the code should be processed with a transpiler. In addition, generators in the writing of asynchronous code are used infrequently, so if you are involved in team development, keep in mind that some programmers may be unfamiliar with them.
In case you decide to better delve into this topic, here and now - excellent materials about the internal structure of the generators.

Async / await


This method is similar to a mixture of generators and promises. You just need to specify, using the async , which function is supposed to be performed asynchronously, and using await , tell the system what part of the code should wait for the permission of the corresponding promise.
As usual, first is a simple example.

 sumTwentyAfterTwoSeconds(10) .then(result => console.log('after 2 seconds', result)) async function sumTwentyAfterTwoSeconds(value) { const remainder = afterTwoSeconds(20) return value + await remainder } function afterTwoSeconds(value) { return new Promise(resolve => {   setTimeout(() => { resolve(value) }, 2000); }); } 

Here the following happens:


Prepare the request function for use in the async/await construction:

 function request(url) { return new Promise(function(resolve, reject) {   const xhr = new XMLHttpRequest();   xhr.onreadystatechange = function(e) {     if (xhr.readyState === 4) {       if (xhr.status === 200) {         resolve(xhr.response)       } else {         reject(xhr.status)       }     }   }   xhr.ontimeout = function () {     reject('timeout')   }   xhr.open('get', url, true)   xhr.send() }) } 

Now we create a function with the async , in which we use the await keyword:

 async function list() { const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const users = await request(userGet) const usersList = JSON.parse(users).items usersList.forEach(async function (user) {   const repos = await request(user.repos_url)     handleRepoList(user, repos) }) } function handleRepoList(user, repos) { const userRepos = JSON.parse(repos) //       console.log(user, userRepos) } 

So, we have an asynchronous list function that will process the request. We also need the async/await construction in a forEach loop to form a list of repositories. Calling all this is very simple:

 list() .catch(e => console.error(e)) 

This approach and the use of promises are my favorite methods of asynchronous programming. Code written with their use is convenient to read and edit. Details about async/await can be found here .

The minus async/await , like the minus of the generators, is that the old browsers do not support this design, and for its use in server development you need to use Node 8. In this situation, again, a transpiler will help, for example, babel .

Results


Here you can see the project code that solves the problem set at the beginning of the material using async/await . If you want to properly deal with what we talked about - experiment with this code and with all the technologies discussed.

Please note that our examples can be improved, made more concise, if you rewrite them using alternative ways to execute queries, like $.ajax and fetch . If you have ideas on how to improve the quality of the code using the methods described above, I will be grateful if you tell me about it.

Depending on the features of the task before you, it may turn out that you will use async / await, callbacks, or some kind of mixture from different technologies. In fact, the answer to the question of which particular method of asynchronous development to choose depends on the characteristics of the project. If a certain approach allows you to solve a problem with a readable code that is easy to maintain, which is understandable (and will be clear after a while) to you and other team members, then this approach is what you need.

Dear readers! What methods of writing asynchronous JavaScript code do you use?

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


All Articles