preface

We all know that Promise does a great job of solving callback hell, but this approach is rife with Promise’s THEN () method, and if the process is complicated, the entire code will be riddled with THEN, not semantically, and the code will not represent the execution process well, Using promise.then is also quite complex, and although the request process has been linearized, the code contains a large number of THEN functions, making the code still not very easy to read. For this reason, ES7 introduced async/await, which is a major improvement over asynchronous JavaScript programming, providing the ability to access resources asynchronously using synchronous code without blocking the main thread, and making code logic clearer.

If you find this helpful, give me a thumbs up with your rich little hands. Thank you very much!

How JavaScript engines implement async/await. You might get a little confused if you go straight to async/await, so let’s go through the bottom technical points and get you straight to how async and await work.

Firstly, it introduces how Generator works, and then explains the underlying mechanism of Generator — Coroutine. Since async/await uses both Generator and Promise technologies, we then use Generator and Promise to analyze how async/await code is written synchronously.

Generators VS coroutines

A generator function is an asterisk function that can be paused and resumed.

function* genDemo() {
    console.log("Proceed with paragraph one.")
    yield 'generator 2'

    console.log("Proceed to paragraph 2.")
    yield 'generator 2'

    console.log("Proceed to paragraph 3.")
    yield 'generator 2'

    console.log("End of execution")
    return 'generator 2'
}

console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')
Copy the code

Execute the above code and observe the output. You will find that the genDemo function is not executed all at once. The global code and genDemo function are executed alternately. This is the nature of generator functions, which can be paused or resumed. Let’s look at how generator functions are used:

  1. Execute a piece of code inside a generator function, and if the yield keyword is encountered, the JavaScript engine returns what follows the keyword externally and suspends execution of the function.
  2. External functions can resume their execution through the next method.

If you are curious about how functions are paused and resumed, let’s take a brief look at how JavaScript engine V8 implements the paused and resumed functions. This will help you understand async/await.

To understand how functions can pause and resume, you first need to understand the concept of coroutines. Coroutines are a much lighter form of existence than threads. You can put the coroutines as A task to run on the thread, A thread can exist on many collaborators, but at the same time in the thread can only perform A collaborators, such as the currently executing A coroutines, to start the B coroutines, then A coroutines needs to be the main thread of control to the B coroutines, this is reflected in A suspended coroutines, B. Coroutine recovery; Similarly, you can start A coroutine from A B coroutine. In general, if we start A coroutine B, we call A coroutine the parent of A coroutine B.

Just as a process can have multiple threads, a thread can have multiple coroutines. Most importantly, coroutines are not managed by the operating system kernel, but are completely controlled by programs (that is, executed in user mode). The benefit of this is that the performance is greatly improved and the resources are not consumed like thread switching.

To give you a better understanding of how coroutines work, I’ve drawn the following “coroutine execution flowchart”, which you can analyze against the code:

The diagram shows the four rules of coroutines:

  1. A coroutine gen is created by calling the generator function genDemo. After creation, the Gen coroutine is not executed immediately.
  2. To get the Gen coroutine to execute, call Gen.next.
  3. While the coroutine is executing, you can use the yield keyword to suspend the execution of the Gen coroutine and return the main information to the parent coroutine.
  4. If, during execution, the coroutine encounters a return keyword, the JavaScript engine terminates the current coroutine and returns the content following the return to the parent coroutine.

The parent coroutine has its own call stack, and the Gen coroutine has its own call stack. How does V8 switch to the parent coroutine’s call stack when the Gen coroutine yields control to the parent? When the parent coroutine restores the Gen coroutine via Gen.next, how do you switch the call stack of the Gen coroutine?

To figure this out, you need to focus on two things.

First point: Gen coroutines and parent coroutines are executed interactively on the main thread, not concurrently, and their previous switches are performed using yield and Gen.next.

Second point: When yield is invoked in a Gen coroutine, the JavaScript engine saves the current stack information for the Gen coroutine and restores the stack information for the parent coroutine. Similarly, when gen.next is executed in a parent coroutine, the JavaScript engine saves the parent coroutine’s call stack information and restores the Gen coroutine’s call stack information.

Just to get a sense of how parent coroutines and gen coroutines switch call stacks

Now that you’ve figured out how coroutines work, in fact, in JavaScript generators are one way to implement coroutines, and you’ll understand what generators are. Next, we’ll use the generator and Promise to modify the initial Promise code. The modified code looks like this:

/ / foo function
function* foo() {
    let response1 = yield fetch('https://www.geekbang.org')
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('https://www.geekbang.org/test')
    console.log('response2')
    console.log(response2)
}

// Execute the code for foo
let gen = foo()
function getGenPromise(gen) {
    return gen.next().value
}
getGenPromise(gen).then((response) = > {
    console.log('response1')
    console.log(response)
    return getGenPromise(gen)
}).then((response) = > {
    console.log('response2')
    console.log(response)
})
Copy the code

As you can see, foo is a generator function that implements asynchronous operations in synchronous code form. But outside of foo, we also need to write a piece of code that executes foo, as shown in the second half of the above code, so let’s examine how this code works.

  • Let gen = foo() is executed first, creating the Gen coroutine. It then gives control of the main thread to the Gen coroutine in the parent by executing Gen.next.
  • When the Gen coroutine gains control of the main thread, it calls the FETCH function to create a Promise object (response1), suspends the gen coroutine with yield, and returns response1 to the parent coroutine.
  • After the parent coroutine resumes execution, the response1.then method is called to wait for the result of the request.
  • After the request initiated through FETCH is completed, the callback function in THEN will be called. After the callback function in THEN gets the result, it will give up the control of the main thread by calling Gen. next and hand the control to gen coroutine to continue the execution of the next request.

The above is a general flow of the implementation of coroutines and promises. Usually, however, we wrap the code that executes the generator into a function and call the function that executes the generator code an executor (see the well-known CO framework), as follows:

function* foo() {
    let response1 = yield fetch('https://www.geekbang.org')
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('https://www.geekbang.org/test')
    console.log('response2')
    console.log(response2)
}
co(foo());
Copy the code

By using generators in conjunction with actuators, it is possible to write asynchronous code in a synchronous manner, which greatly improves the readability of the code.

async/await

While generators have done a good job of satisfying our needs, programmers’ aspirations are endless, and ES7 has introduced async/await as a way to get rid of actuators and generators altogether and achieve more intuitive and concise code. The secret behind async/await technology is Promise and generator applications, microtasks and coroutines at a lower level. To understand how async and await work, we need to analyze async and await separately.

async

So let’s see what async is. According to MDN definition, async is a function that executes asynchronously and implicitly returns a Promise as a result.

Let’s take a look at how to implicitly return a Promise. You can refer to the following code:

async function foo() {
    return 2
}
console.log(foo())  // Promise {<resolved>: 2}
Copy the code

When we execute this code, we can see that foo, which calls the async declaration, returns a Promise object in the resolved state as follows:

Promise {<resolved>: 2}
Copy the code

await

We know that async returns a Promise object, so let’s use this code to see what await is.

async function foo() {
    console.log(1)
    let a = await 100
    console.log(a)
    console.log(2)}console.log(0)
foo()
console.log(3)
Copy the code

Looking at the code above, can you tell what the printed content is? This starts with an analysis of what happens when async and await are combined. Before going into detail, let’s look at the overall execution flow of this code from the perspective of coroutines:

Combined with the above figure, let’s analyze the execution process of async/await.

First, execute the console.log(0) statement and print out a 0.

This is followed by executing foo, which is flagged by async, so when entering foo, the JavaScript engine saves the current call stack and so on, and then executes console.log(1) in foo, printing out 1.

We are going to await ‘await 100’ in foo. This is the focus of our analysis because the JavaScript engine is doing too much behind the scenes for us to await ‘await 100’. So let’s break this statement down. Let’s take a look at what JavaScript does.

When executed to await 100, a Promise object is created by default, as shown below

let promise_ = new Promise((resolve,reject){
  resolve(100)})Copy the code

During the creation of the promise_ object, we see that the resolve function is called in the executor function and the JavaScript engine submits the task to the microtask queue.

The JavaScript engine then suspends execution of the current coroutine, transfers control of the main thread to the parent coroutine, and returns the promise_ object to the parent coroutine.

Now that control of the main thread has been handed over to the parent coroutine, one thing the parent coroutine does is call promise_. Then to monitor the promise state. To continue the parent coroutine process, here we execute console.log(3) and print out 3.

The parent coroutine will then execute to the checkpoint of the microtask and then execute the microtask queue (resolve(100)), which will trigger the callback function in promise_. Then as follows:

promise_.then((value) = >{
   // After the callback function is activated
  // Give the main thread control to the Foo coroutine and pass the vaule value to the coroutine
})
Copy the code

When activated, the callback gives control of the main thread to the coroutine of foo, along with the value.

When the foo coroutine is activated, it assigns the value to variable A, and then the Foo coroutine executes the following statement, giving control back to the parent coroutine.

This is the execution flow of await/async. It is because async and await are doing so much work behind the scenes that we can write asynchronous code synchronously.

summary

Promise’s programming model is still riddled with a lot of THEN methods, and while it solves callback hell, it’s still semantically flawed, with a lot of THEN functions in the code, which is where async/await comes in.

With async/await it is possible to write asynchronous code in the style of synchronous code. This is because the basic technology of async/await uses generators and promises. Generators are implementations of coroutines, with which generator functions can be paused and resumed.

In addition, the V8 engine does a lot of syntactic wrapping for async/await, so understanding the code behind async/await will help you understand more. Async /await is undoubtedly a very big innovation in the field of asynchronous programming and also a mainstream programming style in the future.

In addition to JavaScript, Python, Dart, C#, and other languages have introduced async/await, which not only makes code look cleaner, but also ensures that the function always returns promises.

Strong friends

Like to make some friends like me in the front of the advanced friends, sometimes a person’s road is not easy to go, many people go together is interesting. link

If you find this helpful, give me a thumbs up with your rich little hands. Thank you very much!