1. The ultimate solution for asynchronous programming

As mentioned at the end of the previous article, async/await is the ‘ultimate’ solution for asynchronous programming. The ultimate word is that using async/await to operate asynchronously is logically and semantically infinitely similar to synchronous operations (of course, it is only formally similar without changing the nature of asynchracy, which will be explained later).

Let’s take a look at the code that used the Generator function to control the asynchronous process

function* gen() {
  const res1 = yield promisify_readFile("./text1.txt");
  console.log(res1.toString());
  const res2 = yield promisify_readFile("./text2.txt");
  console.log(res2.toString());
}
co(gen);
Copy the code

The following implementation uses async/await

async function asyncReadFile() {
  const res1 = await promisify_readFile("./text1.txt");
  console.log(res1.toString());
  const res2 = await promisify_readFile("./text2.txt");
  console.log(res2.toString());
}
asyncReadFile()
Copy the code

As you can see, asynchronous process processing with async/await does not formally require an executor. Functions can be executed as normal functions, which means that async functions have built-in executors of Generator functions. Semantically, the async keyword denotes an asynchronous operation within a function, and the await keyword denotes waiting for an asynchronous operation to complete, which is friendlier than Generator functions that use * declarations and yield expressions to partition state.

The following details the characteristics of async functions and await keywords.

2. Features of async function and await keyword

2.1 Return value of async function

Async functions return Promise objects, so you can specify then, catch, and other methods for async functions.

asyncReadFile().then(() => {
  console.log("end");
});
Copy the code

Since async returns a Promise object, what determines its outcome and state

  • When the async function returns a value, that parameter becomes the successful callback of the then method (that is, the result value of the Promise), and the state changes to success.
const promisifyTimeOut = () => { return new Promise((resolve) => { setTimeout(() => { resolve('timeOut') }, 500); }) } const asyncTimeOut = async () => { const res = await promisifyTimeOut() return res }; asyncTimeOut().then( (res) => { console.log('success' + res); }, (r) => { console.log('err' + r); }); //success timeOutCopy the code
  • When an async function throws an error internally, the state immediately changes to failure and the failure callback or catch method of the then method is executed.
const asyncTimeOut = async () => { const res = await promisifyTimeOut() throw res }; asyncTimeOut().then( (res) => { console.log('success' + res); }, (r) => { console.log('err' + r); }); // err timeOutCopy the code

This allows for error handling of async functions, which will be described later.

2.2 Features of await keyword
  • Await the await command can only be used in async functions, normal functions will report an error.
  • Await the await command returns the result value of the Promise if it is a Promise object, or the corresponding value if it is not a Promise object.
(async function(){
    const res1 = await Promise.resolve('foo')
    console.log(res1)
    const res2 = await 'bar'
    console.log(res2)
})()
// foo
// bar
Copy the code

3. Error handling of async functions

As mentioned earlier, when an async function throws an error internally, its state immediately changes to failed and a failed callback is executed (assuming a failed callback is specified). Therefore any Promise state after the await keyword changes to Rejected causes the async function to immediately abort.

const promisifyTimeOut = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("some err");
    }, 500);
  });
};
const asyncTimeOut = async () => {
  await promisifyTimeOut();
  console.log("foo");
};
asyncTimeOut().catch((r) => console.log(r));
// some err
Copy the code

In the above code, async after await throws an error and async function breaks execution causing Foo not to be printed. If you don’t want an async function to terminate when an error is thrown inside it, you can place a Promise package that may throw an error in a try… In the catch block, or to specify a failure callback for a Promise that may throw an error (specifying either the then or catch methods), a try… Catch is used as an example.

const asyncTimeOut = async () => {
  try {
    await promisifyTimeOut()
  } catch (error) {
    console.log(error)
  }
  console.log("foo");
};
asyncTimeOut().catch((r) => console.log(r))
// some err
// foo
Copy the code

If the above two methods are used for error handling, the failure callback specified by the async function will not take effect (assuming the error is not thrown in a catch statement or a Promise failure callback). In addition, multiple await statements can be wrapped together in a try… Catch for unified error handling.

4. Implementation principle of Async function

In fact, after the previous discussion of CO module and the introduction of the characteristics of async function, we can know that async/await is the syntactic sugar of Generator function, and we only need to encapsulate according to its characteristics, as follows.

  • Async function built-in Generator function executor.
  • Async returns a Promise and waits until all internal promises are fulfilled before changing the state. The async function throws an error and the state changes to Rejected immediately.

We assume that async’s built-in executor is called the spawn function, so async functions are structured like this

const async = (gen) => {
  return () => {
    return spawn(gen);
  };
};
Copy the code

Next, implement the actuator, which is basically the same as the CO module discussed earlier

function spawn(genF) { return new Promise(function (resolve, reject) { const gen = genF(); function step(data) { let res; try { res = gen.next(data); } catch (e) {// Internal throw error status changes to rejet return reject(e); } if (res.done) { return resolve(res.value); } // Reject promise.resolve (res.value). Then (step, (r) => reject(r)); } step(); }); }Copy the code

Here’s a quick test

const promisify = (data) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(data); }, 300); }); }; function* testGen() { const res1 = yield promisify(1); console.log(res1); const res2 = yield promisify(2); console.log(res2); return res2; } const async = (gen) => { return () => { return spawn(gen); }; }; Const asyncFoo = async(testGen); Const res = asyncFoo(); setTimeout(() => { console.log(res); }, 1000); // 1 // 2 // Promise { 2 }Copy the code

5. Async function and execution environment stack

As we learned from the previous discussion of JavaScript execution contexts, the JavaScript engine creates a stack of execution contexts before executing code, and then creates and pushes the global execution context as the bottom of the stack. Each time a function is executed, the execution context is created for the function and pushed to the execution environment stack, forming a context stack composed of execution contexts. Each context has a variable object associated with it, containing the variables, functions, parameters, and so on of the current context. The stack is a “last in, first out” data structure, so the resulting context executes first and exits the stack, and then executes the context below it. The bottom of the stack is always the global context, and when the browser window closes, the global context is removed from the stack.

This is not the case with Generator functions. The context generated by the execution of Generator functions is temporarily removed from the stack when yield is used, but does not disappear, and all variables and objects in the variable object are frozen in their current state. When the next command is executed, the execution context is readded to the execution environment stack, and frozen variables and objects resume execution. Async functions are syntactic sugar of Generator functions, and thus have the same property that async functions retain the run stack.

Here is an example to illustrate the comparison

const timeOut = () => { return new Promise((resolve) => { setTimeout(() => { resolve(); }, 500); }); }; (function () { for (let i = 0; i < 3; i++) { timeOut().then(() => { console.log(i); }); } console.log("end"); }) (); // end // 0 1 2Copy the code

Since the promise.then method does not freeze the current context, the loop is not affected, and because the callback in the then method executes asynchronously, all three log statements are queued almost simultaneously. The final result is the above execution results.

Let’s rewrite the above code with async/await

(async function () { for (let i = 0; i < 3; i++) { await timeOut(); console.log(i); } console.log("end"); }) (); // 0 1 2 endCopy the code

The code above prints 0, 1, 2 ends.

The reason is that the async function can retain the current context, and when encountering await command, all the state of the current context is frozen, and all the code including the for loop will suspend execution, thus resulting in the above execution result.

In fact, this feature can be interpreted as saying that all code following the await command enters the asynchronous task queue. Await is the syntactic sugar of THEN, and all the code after it goes into the promise.then callback and is executed asynchronously in the task queue.

Using this, we can implement the dormancy device.

function sleep(interval) { return new Promise((resolve) => { setTimeout(resolve, interval); }); } // async function async (timeOut) {await sleep(timeOut); console.log("foo!" ); } Async(1000); // Print foo after one second!Copy the code

There are two points to note about the above features

1. Await statement freezes only the context of async function, i.e. execution of code following async function will not be blocked. This shows that async/await is written just like synchronous code and the nature of asynchrony has not changed.

async function Async(timeOut) { await sleep(timeOut); console.log("foo!" ); } Async(0) console.log('end! ') // end! // foo!Copy the code

2. As stated above, all code will be frozen after the await keyword is encountered, so asynchronous tasks under the await statement will wait until the asynchro of the await statement is finished. This is very friendly for asynchronous (secondary) handling of dependencies. But again, if two asynchrons have no secondary relationship, try not to write this because it will block. You can use something like promise.all () to have them execute concurrently rather than sequentially.

function sleep(interval) { return new Promise((resolve) => { setTimeout(() => { console.log("foo!" ); resolve(); }, interval); }); } async function Async() { await Promise.all([sleep(500), sleep(500)]); console.log("end"); } Async(); // Two asynchronous concurrent executions print foo almost simultaneously!Copy the code