Browser Principle 19 # how does the JavaScript engine implement async / await to write asynchronous code synchronously?

Posted by AnAmericanGunner on Sun, 20 Feb 2022 17:37:55 +0100

explain

Learning notes of browser working principle and practice column

Why async / await

Let's start with a request for remote resources using fetch:

fetch request example

fetch('https://www.geekbang.org')
	.then((response) => {
	    console.log(response)
	    return fetch('https://www.geekbang.org/test')
	}).then((response) => {
	    console.log(response)
	}).catch((error) => {
	    console.log(error)
})

Console effect:

From this Promise code, we can see that Promise. Com is used Then is also quite complex. Although the whole request process has been linearized, the code contains a large number of then functions, which makes the code still not easy to read. For this reason, ES7 introduces async / await, which provides the ability to use synchronous code to access resources asynchronously without blocking the main thread, and makes the code logic clearer.

Transforming code with async / await

async function foo(){
  try{
    let response1 = await fetch('https://www.geekbang.org')
    console.log('response1')
    console.log(response1)
    let response2 = await fetch('https://www.geekbang.org/test')
    console.log('response2')
    console.log(response2)
  }catch(err) {
       console.error(err)
  }
}
foo()

Generator VS coroutine

Why do you suddenly say this again?

Because async / await uses two technologies, Generator and Promise, and the underlying implementation mechanism of Generator is Coroutine.

generator function

The generator function is a function with an asterisk and can pause and resume execution.

function* genDemo() {
    console.log("Start the first paragraph")
    yield 'generator 1'

    console.log("Start the second paragraph")
    yield 'generator 2'

    console.log("Commencement of the third paragraph")
    yield 'generator 3'

    console.log("end of execution")
    return 'generator 4'
}

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')

The results are as follows:

  • Execute a piece of code inside the generator function. If the yield keyword is encountered, the JavaScript engine will return the content behind the keyword to the outside and pause the execution of the function.
  • The external function can resume the execution of the function through the next method.

So how does JavaScript engine V8 implement the pause and resume of a function?

To understand this problem, we need to understand the concept of collaborative process

Synergetic process

A coroutine is a more lightweight existence than a thread.

You can think of a coroutine as a task running on a thread. There can be multiple coroutines on a thread, but only one coroutine can be executed on a thread at the same time. The cooperation process is not managed by the operating system kernel, but completely controlled by the program (that is, executed in user mode). The advantage of this is that the performance has been greatly improved and will not consume resources like thread switching.

How to understand that only one coordination process can be executed?

For example, the current execution is A process. To start B process, A process needs to hand over the control of the main thread to B process, which is reflected in that A process suspends execution and B process resumes execution; It can also be started from process A and process B. Usually, if we start the B process from the A process, we call the A process the parent process of the B process.

Process execution flow chart:

Four rules of collaborative process:

  1. Create a co process Gen by calling the generator function genDemo. After creation, the gen co process is not executed immediately.
  2. To make the gen coroutine execute, you need to call gen.next.
  3. When the collaboration is executing, you can use the yield keyword to pause the execution of the gen collaboration and return the main information to the parent collaboration.
  4. If the return keyword is encountered during the execution of the collaboration, the JavaScript engine will end the current collaboration and return the content after return to the parent collaboration.

How does V8 switch to the call stack of the parent and gen coroutines?

  1. When the yield method is invoked in the gen Association, the JavaScript engine saves the current call stack information of the gen coopera and restores the calling stack information of the parent association.
  2. When gen.next is executed in the parent process, the JavaScript engine will save the call stack information of the parent process and recover the call stack information of the gen process.
  3. Gen and parent processes are executed interactively on the main thread, not concurrently. Their previous switching is completed through yield and gen.next.

Switching between gen and parent processes:

In JavaScript, a generator is an implementation of a coroutine.

Use generators and Promise to transform code

// foo is a generator function that implements asynchronous operations in the form of synchronous code
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)
}

// Code to execute foo function
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)
})
  1. First, let gen = foo() is executed to create the gen collaboration.
  2. Then, execute gen.next in the parent process to give the control of the main thread to the gen process.
  3. After the gen coroutine obtains the control right of the main thread, it calls the fetch function to create a Promise object response1, then pauses the execution of the gen coroutine through yield and returns response1 to the parent coroutine.
  4. After the parent session is resumed, call response1.. The then method waits for the result of the request.
  5. 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 over the control to the gen process to continue to execute the next request.

Actuator

Encapsulate the code of the generator into a function, and call the function that executes the generator code the executor (refer to the famous co framework)

For the co framework, please refer to: co module

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());

async / await

The secret behind async / await technology is Promise and generator applications. At the lower level, it is micro task and collaborative application.

async

MDN: async function

According to the definition of MDN, async is a function that executes asynchronously and implicitly returns Promise as the result.

Implicitly return Promise:

async function foo() {
    return 2
}
console.log(foo())

await

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

Execution result:

Execution flow chart:

promise_. Callback function in then

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
})

Topics: Javascript