Detailed Explanation of Asynchronous Programming Knowledge Points in nodejs

  • 2021-10-24 18:42:27
  • OfStack

Brief introduction

Because javascript is single-threaded by default, this means that code cannot create new threads to execute in parallel. However, for javascript, which started to run in a browser, the single-threaded synchronous execution environment obviously can't meet the functions of responding to users such as page clicking and mouse movement. Therefore, the browser implemented a set of API, which can make javascript asynchronously respond to the request events of the page by callback.

A step further, nodejs introduces non-blocking I/O, which extends the concept of asynchronism to file access, network calls, and so on.

Today, we will discuss the advantages, disadvantages and development trends of various asynchronous programming in depth.

Synchronous asynchronous and blocking non-blocking

Before discussing asynchronous programming of nodejs, let's discuss one confusing concept, that is, synchronous, asynchronous, blocking and non-blocking.

The so-called blocking and non-blocking refers to whether a process or thread needs to wait when operating or reading and writing data, and whether other operations can be carried out in the process of waiting.

If there is a need to wait, and the thread or process can't do anything but wait foolishly during the waiting, then we say that the operation is blocked.

On the contrary, if a process or thread can perform other operations during the operation or data reading and writing, then we say that this operation is non-blocking.

Synchronous and asynchronous refer to the way data is accessed. Synchronous refers to the need to actively read data, which may be blocked or non-blocked. Asynchronous means that it does not need to read data actively, but is a passive notification.

Obviously, the callback in javascript is a passive notification, which we can call an asynchronous call.

Callbacks in javascript

Callbacks in javascript are a very typical example of asynchronous programming:


document.getElementById('button').addEventListener('click', () => {
 console.log('button clicked!');
})

In the above code, we added an click event listener for button. If the click event is monitored, the callback function will be launched and the corresponding information will be output.

The callback function is a normal function, except that it is passed as an argument to addEventListener and is called only when an event is triggered.

The setTimeout and setInterval we talked about in the previous article are actually asynchronous callback functions.

Error handling of callback function

How do you handle callback error messages in nodejs? nodejs adopts a very clever method. In nodejs, the first parameter in any callback function is an error object, and we can handle the error accordingly by judging whether the error object exists or not.


fs.readFile('/ Documents .json', (err, data) => {
 if (err !== null) {
 // Handling errors 
 console.log(err)
 return
 }

 // If there are no errors, the data is processed. 
 console.log(data)
})

Callback to hell

Although the callback of javascript is very excellent, it effectively solves the problem of synchronous processing. Unfortunately, if we need to rely on the return value of the callback function for the next step, we will fall into this callback hell.

Calling callback hell is a bit exaggerated, but it also reflects the problems of callback function from one aspect.


fs.readFile('/a.json', (err, data) => {
 if (err !== null) {
 fs.readFile('/b.json',(err,data) =>{
  //callback inside callback
 })
 }
})

How to solve it?

Don't be afraid that Promise introduced by ES6 and Async/Await introduced by ES2017 can solve this problem.

Promise in ES6

What is Promise

Promise is a solution for asynchronous programming, which is more reasonable and powerful than the traditional solution "callback functions and events".

The so-called Promise is simply a container, which holds the result of an event (usually an asynchronous operation) that will end in the future.

Syntactically speaking, Promise is an object from which messages for asynchronous operations can be obtained.

Characteristics of Promise

Promise has two characteristics:

1. The state of the object is not affected by the outside world.

The Promise object represents an asynchronous operation with three states: Pending (in progress), Resolved (completed, also known as Fulfilled), and Rejected (failed).

Only the result of an asynchronous operation can determine which state is currently in, and no other operation can change this state.

2, 1 Once the state changes, it will not change again, and this result can be obtained at any time.

There are only two possibilities for the state change of the Promise object: from Pending to Resolved and from Pending to Rejected.

This is completely different from the event (Event). The characteristic of the event is that if you miss it and listen, you will not get the result.

Advantages of Promise

Promise expresses asynchronous operations as the flow of synchronous operations, avoiding layers of nested callback functions. The Promise object provides a unified interface, making it easier to control asynchronous operations.

Disadvantages of Promise

Unable to cancel Promise, it will be executed immediately as soon as it is newly created, and cannot be cancelled halfway. If the callback function is not set, the errors thrown inside Promise will not be reflected outside. When in the Pending state, it is impossible to know which 1 stages (just started or about to be completed) are currently progressing.

Usage of Promise

The Promise object is a constructor that generates an Promise instance:


var promise = new Promise(function(resolve, reject) { 
// ... some code 
if (/*  Asynchronous operation succeeded  */){ 
resolve(value); 
} else { reject(error); } 
}
);

promise can be connected with then operation, and then operation can be connected with two function parameters. The parameter of the first function is value of resolve when Promise is constructed, and the parameter of the second function is error of reject when Promise is constructed.


promise.then(function(value) { 
// success 
}, function(error) { 
// failure }
);

Let's look at a specific example:


function timeout(ms){
 return new Promise(((resolve, reject) => {
  setTimeout(resolve,ms,'done');
 }))
}

timeout(100).then(value => console.log(value));

An setTimeout method is called in Promise, and the resolve method is triggered regularly, and the parameter done is passed in.

Finally, the program outputs done.

Execution sequence of Promise

Promise1 will be implemented as soon as it is created. However, the method in Promise. then will be called again after 1 call cycle. Let's look at the following example:


let promise = new Promise(((resolve, reject) => {
 console.log('Step1');
 resolve();
}));

promise.then(() => {
 console.log('Step3');
});

console.log('Step2');

Output

Step1
Step2
Step3

async and await

Promise is certainly good, and we have turned callback hell into a chained call. We use then to connect multiple Promise, and the result of the first promise resolve is the parameter of then in the next promise.

What are the disadvantages of chain calling?

For example, from an promise, resolve has a value, and we need to process some business logic according to this value.

If this business logic is very long, we need to write very long business logic code in the next then. This makes our code look very redundant.

So is there any way to directly return the results of resolve in promise?

The answer is await.

When promise is preceded by await, the calling code stops until promise is resolved or rejected.

Note that await1 must be placed in the async function. Let's look at an example of async and await:


const logAsync = () => {
 return new Promise(resolve => {
 setTimeout(() => resolve(' Mark '), 5000)
 })
}

Above we defined an logAsync function, which returns an Promise, because the Promise uses setTimeout to resolve internally, so we can think of it as asynchronous.

To get the value of resolve using await, we need to put it in a function of async:


const doSomething = async () => {
 const resolveValue = await logAsync();
 console.log(resolveValue);
}

Execution sequence of async

await is actually waiting for the resolve result of promise. Let's combine the above examples:


const logAsync = () => {
 return new Promise(resolve => {
  setTimeout(() => resolve(' Mark '), 1000)
 })
}

const doSomething = async () => {
 const resolveValue = await logAsync();
 console.log(resolveValue);
}

console.log('before')
doSomething();
console.log('after')

The above example outputs:

before
after
Mark

As you can see, aysnc is executed asynchronously, and its order is after the current cycle.

Characteristics of async

async causes all subsequent functions to become Promise, even if the following function returns Promise if it is not shown.


fs.readFile('/ Documents .json', (err, data) => {
 if (err !== null) {
 // Handling errors 
 console.log(err)
 return
 }

 // If there are no errors, the data is processed. 
 console.log(data)
})
0

Because only Promise can be followed by then, we can see that async encapsulates a normal function into an Promise:


fs.readFile('/ Documents .json', (err, data) => {
 if (err !== null) {
 // Handling errors 
 console.log(err)
 return
 }

 // If there are no errors, the data is processed. 
 console.log(data)
})
1

Summarize

promise avoids callback hell by rewriting callback inside callback into a chained call form of then.

But chain calls are not easy to read and debug. So async and await appeared.

async and await change the chain call into a syntax similar to the sequential execution of programs, which makes it easier to understand and debug.

Let's look at a comparison. First, look at the use of Promise:


fs.readFile('/ Documents .json', (err, data) => {
 if (err !== null) {
 // Handling errors 
 console.log(err)
 return
 }

 // If there are no errors, the data is processed. 
 console.log(data)
})
2

Rewrite it as async and await:


const getUserInfo = async () => {
 const response = await fetch('/users.json') //  Get a list of users 
 const users = await response.json() //  Analyse  JSON
 const user = users[0] //  Select 1 Users 
 const userResponse = await fetch(`/users/${user.name}`) //  Get user data 
 const userData = await userResponse.json() //  Analyse  JSON
 return userData
}

getUserInfo()

You can see that the business logic becomes clearer. At the same time, we get a lot of intermediate values, which is convenient for us to debug.


Related articles: