Original text: blog. Bitsrc. IO/understandi… Translated by Arfat Salman: The front end is white
First we’ll talk about callback functions. Callback functions are nothing special, just functions that will be executed at some point in the future. Because of the asynchronous nature of JavScript, callbacks are required in many places where results are not immediately available
Here is an example of node.js asynchronously reading a file:
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
});
Copy the code
Problems arise when we want to perform multiple asynchronous operations. Imagine the following scenario (all operations are asynchronous) :
- We query the user in the database
Arfat
. We readprofile_img_url
And from thesomeServer.com
Get the image. - Once the image is taken, we convert it to another format, such as PNG to JPEG.
- If the conversion is successful, we send an E-mail to the user.
- in
transformations.log
Record the task with a time stamp
The code looks like this:
Note the hierarchy of the nested end of the callback function}), which has been nicknamed callback hell or callback pyramid. The disadvantage is that –
- Bad code readability
- Error handling is complex and often results in bad code.
To address these issues, JavaScript makes promises. Now we can use the chain structure instead of the nested structure of callback functions. Here’s an example
The process is now structured top-down rather than left to right, which is a plus. But promise still has some drawbacks
- At the end of each
.then
We still have to deal with callbacks - Compared to using normal
try/catch
We’re going to use.catch()
Handling errors - Multiple processes are processed sequentially in a loop
promises
It’s very painful. It’s not intuitive
Let’s demonstrate the last disadvantage:
challenge
Suppose we have a for loop that prints 0 to 10 at random intervals (0 to n seconds). We need to print them out in order 0 to 10 using promises. For example, if 0 takes 6 seconds to print and 1 takes 2 seconds to print, then 1 should wait for 0 to finish printing before printing, and so on.
Do not use async/await or.sort, we will fix this later
Async function
ES2017(ES8) introduces async functions that make it easy to apply promises
- It is important to note that:
async
The use of functions is based onpromise
- They are not completely different concepts
- Can be considered as a kind of basis
promise
An alternative to asynchronous code async/await
You can avoid usingpromise
Chain calls- The code executes asynchronously and looks synchronous
Therefore, to understand async/await you must first understand promises
grammar
Async /await contains two keywords async and await. Async is used to make functions run asynchronously. Async allows us to use await in functions, otherwise it is a syntax error to use await anywhere.
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async() = > {// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
Copy the code
Notice that the async keyword precedes the function declaration in the function declaration. In the arrow function, the async keyword is placed between = and parentheses.
Async functions can also be used as methods on objects or in class declarations.
// As an object's method
const obj = {
async getName() {
return fetch('https://www.example.com'); }}// In a class
class Obj {
async getResource() {
return fetch('https://www.example.com'); }}Copy the code
Note: class constructor andgetters/setters
Async functions cannot be used.
Semantics and evaluation criteria
Async is a normal JavaScript function with the following differences
Async functions always return a Promise object.
async function fn() {
return 'hello';
}
fn().then(console.log)
// hello
Copy the code
The function fn returns ‘hello’, and since we are using the async keyword, ‘hello’ is wrapped as a Promise object (implemented via the Promise constructor)
This is an alternative implementation that does not use async
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// hello
Copy the code
In the code above, we returned a Promise object manually without using the async keyword
More precisely, the return value of the async function is wrapped in promise.resolve.
Resolve returns a promised value if the return value is the original value, or if the return value is a Promise object, the Promise object is returned
// in case of primitive values
const p = Promise.resolve('hello')
p instanceof Promise;
// true
// p is returned as is it
Promise.resolve(p) === p;
// true
Copy the code
What if an async function throws an error?
For instance,
async function foo() {
throw Error('bar');
}
foo().catch(console.log);
Copy the code
If the error is not caught, the foo() function returns a promise with the status Rejected. Unlike promise.resolve, promise.reject wraps the error and returns. See error Handling later for details.
The end result is that whatever result you return, you will get a promise from the Async function.
Async functions pause when encountering await < expression >
Await applies to an expression. When the expression is a PROMISE, the async function suspends execution until the Promise state changes to Resolved. When the expression is a non-PROMISE value, promise.resolve is used to convert it to a PROMISE, and then the state changes to Resolved.
// utility function to cause delay
// and get random value
const delayAndGetRandom = (ms) = > {
return new Promise(resolve= > setTimeout(
(a)= > {
const val = Math.trunc(Math.random() * 100);
resolve(val);
}, ms
));
};
async function fn() {
const a = await 9;
const b = await delayAndGetRandom(1000);
const c = await 5;
await delayAndGetRandom(1000);
return a + b * c;
}
// Execute fn
fn().then(console.log);
Copy the code
Let’s look at the FN function line by line
- The first line of code when the function executes
const a = await 9
, the inside will be resolved toconst a = await Promise.resolve(9)
- Because it uses
await
, so function execution is paused until the variablea
Get a value, which in the promise will resolve to 9 delayAndGetRandom(1000)
Can makefn
The function pauses until 1 second laterdelayAndGetRandom
Resolve, therefore,fn
Execution of the function is effectively paused for 1 second- In addition,
delayAndGetRandom
Returns a random number. Whatever is passed in resolve is assigned to the variable, rightb
. - Again, variables
c
The value is 5, then useawait delayAndGetRandom(1000)
There was another delay of one second. We don’t use it in this line of codePromise.resolve
The return value. - And finally, we calculate
a + b * c
Result, usePromise.resolve
Package and return
Note: If the pause and resume of functions here reminds you of ES6 generators, it’s because Generator has many advantages
The solution
Let’s use async/await to solve the hypothetical problem raised at the beginning of the article:
We define an async function finishMyTask and await results from queryDatabase, sendEmail, logTaskInFile with await
If we compare the async/await solution with the one using Promise, we see a similar amount of code. But async/await makes code look simpler without memorizing multiple layers of callback functions and.then /.catch.
Now let’s solve the problem of printing numbers. There are two solutions
const wait = (i, ms) = > new Promise(resolve= > setTimeout((a)= > resolve(i), ms));
// Implementation One (Using for-loop)
const printNumbers = (a)= > new Promise((resolve) = > {
let pr = Promise.resolve(0);
for (let i = 1; i <= 10; i += 1) {
pr = pr.then((val) = > {
console.log(val);
return wait(i, Math.random() * 1000);
});
}
resolve(pr);
});
// Implementation Two (Using Recursion)
const printNumbersRecursive = (a)= > {
return Promise.resolve(0).then(function processNextPromise(i) {
if (i === 10) {
return undefined;
}
return wait(i, Math.random() * 1000).then((val) = > {
console.log(val);
return processNextPromise(i + 1);
});
});
};
Copy the code
It’s even simpler if you use async
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i); }}Copy the code
Error handling
As we learned in the syntax section, an uncaught Error() is wrapped in a Rejected Promise, but we can use try-catch to handle errors synchronously in async functions. Let’s start with this useful function
async function canRejectOrReturn() {
// wait one second
await new Promise(res= > setTimeout(res, 1000));
// Reject with ~50% probability
if (Math.random() > 0.5) {
throw new Error('Sorry, number too big.')}return 'perfect number';
}
Copy the code
CanRejectOrReturn () is an asynchronous function, either resolve ‘perfect number’ or reject Error(‘Sorry, number too big’)
Look at the code below
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'error caught'; }}Copy the code
Since we’re waiting for canRejectOrReturn to execute, its rejection is translated into an error throw, and the catch is executed, which means foo’s resolve is either undefined (because we didn’t return a value in the try), Either resolve ‘error caught’. Foo is never rejected because we use a try-catch in foo.
Another example
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'error caught'; }}Copy the code
Pay attention to. Instead of waiting for canRejectOrReturn, foo will either resolve ‘perfect number’ or reject Error(‘Sorry, number too big’), The catch statement is not executed
Since we return the promise object returned by canRejectOrReturn, foo’s final state is determined by the state of canRejectOrReturn, You can break the return canRejectOrReturn() into two lines to get a clearer idea, noting that the first line has no await
try {
const promise = canRejectOrReturn();
return promise;
}
Copy the code
Let’s look at using return and await together
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'error caught'; }}Copy the code
In this case foo resolve ‘perfect number’ or resolve ‘error caught’ with no rejection like the above example with await only Here we resolve the value returned by canRejectOrReturn, not undefined
We can also split return await canRejectOrReturn()
try {
const value = await canRejectOrReturn();
return value;
}
// ...
Copy the code
Common mistakes and pitfalls
Due to the intricacies of the operation between Promise and async/await. There may be some hidden errors, let’s take a look at –
No use of await
Sometimes we forget to use await before promises, or forget to return
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught'; }}Copy the code
Note that foo will always resolve undefined if we do not use await or return, and will not wait a second, but the promise in canRejectOrReturn() does get executed. If there is a side effect, also can produce, if throw an error or reject, UnhandledPromiseRejectionWarning can produce
Use async functions in callbacks
We often use async functions as callbacks in.map and.filter, assuming we have a function fetchPublicReposCount(username) that gets the number of public repositories owned by a Github user. We want to get the number of open repositories for three different users, so let’s look at the code —
const url = 'https://api.github.com/users';
// Utility fn to fetch repo counts
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}
Copy the code
We want to get the number of warehouses for [‘ArfatSalman’, ‘Octocat ‘,’ Norvig ‘] and do this:
const users = [
'ArfatSalman'.'octocat'.'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
Copy the code
Note that async in the.map callback, we want the COUNTS variable to contain the number of storehouses. However, as we saw earlier, async returns a Promise object, so COUNTS is actually a Promises array, This array contains the promise returned each time the function is called to get the number of user repositories,
Excessive use of await sequentially
async function fetchAllCounts(users) {
const counts = [];
for (let i = 0; i < users.length; i++) {
const username = users[i];
const count = await fetchPublicReposCount(username);
counts.push(count);
}
return counts;
}
Copy the code
We manually retrieved each count and saved them in the COUNTS array. The problem with the program is that the count of the second user can be fetched only after the first user’s count is fetched. Only one warehouse can be retrieved at a time.
If a FETCH operation takes 300 ms, the fetchAllCounts function takes about 900 ms. Thus, the program time increases linearly as the number of users increases. Because there is no dependency between getting the number of repositories exposed by different users, we can parallel the operations.
We can acquire users simultaneously rather than sequentially. We’ll use.map and promise.all.
async function fetchAllCounts(users) {
const promises = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
return Promise.all(promises);
}
Copy the code
Promise.all takes an array of Promise objects as input and returns a Promise object as output. When all promise objects are switched to Resolved, the return value is an array of resolved results, and when it fails, the first rejected state is returned. As long as one promise object is rejected, Promise.all returns the value corresponding to the first Promise object rejected. However, running all promises at the same time may not work. If you want to make promises in bulk. Refer to p-Map for quantity-controlled concurrent operations.
conclusion
Async functions are very important. With the introduction of Async Iterators, Async functions will be more and more widely used. It is essential for modern JavaScript developers to master and understand async functions. I hope this article has been helpful to you