Previously, I wrote an article “This time, Thoroughly Understand the Promise Principle”, which analyzed the relevant principles of Promise, and I got a good response. This time, I also wrote down the relevant knowledge I learned.

We all know that promises solve the problem of callback hell, but if you’re dealing with a complex business, the code can still be hard to read with a lot of then functions.

For this reason, ES7 introduces async/await, which is a major improvement in JavaScript asynchronous programming. It provides the ability to access resources asynchronously using synchronous code without blocking the main thread, makes the code logic clearer, and also supports try-catch to catch exceptions. Very much in line with human linear thinking.

So, look at how async/await can be implemented. In general, async is the syntax sugar for Generator functions and makes improvements to them.

Introduction to Generator Functions

A Generator function is a state machine that encapsulates multiple internal states. Executing the Generator function returns a traverser object that can traverse each state inside the Generator function in turn, but only calling the next method will traverse the next internal state, so it actually provides a function that can pause execution. The yield expression is the pause flag.

Here’s the code:

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
Copy the code

Call and run results:

hw.next()// { value: 'hello', done: false }
hw.next()// { value: 'world', done: false }
hw.next()// { value: 'ending', done: true }
hw.next()// { value: undefined, done: true }
Copy the code

As you can see, the Generator function is not executed when it is called, but only when the next method is called and the internal pointer points to the statement, which means the function can pause or resume execution. Each time the next method of the traverser object is called, it returns an object with two properties, value and done. The value attribute represents the value of the current internal state and is the value of the expression following the yield expression; The done property is a Boolean that indicates whether the traversal is complete.

The Generator function is suspended to restore the execution principle

To understand why functions can pause and resume, you first need to understand the concept of coroutines.

When a thread (or function) is in the middle of its execution, it can pause, hand over execution to another thread (or function), and resume execution later when it is withdrawn. A thread (or function) that can execute in parallel and swap execution rights is called a coroutine.

Coroutines are more lightweight than threads. Ordinary threads are preemptive and compete for CPU resources, while coroutines are cooperative. Coroutines can be viewed as tasks running on threads. Multiple coroutines can exist on a thread, but only one coroutine can be executed on the thread at the same time. Its running process is roughly as follows:

  1. coroutinesAStart to perform
  2. coroutinesAAt a certain stage of execution, a pause is entered, and the execution power is transferred to the coroutineB
  3. coroutinesBUpon completion or suspension of execution, the execution authority shall be returnedA
  4. coroutinesAResume execution

The coroutine pauses when it encounters the yield command, waits until the execution returns, and then continues from where it was suspended. The big advantage is that the code is written very much like a synchronous operation, and if you remove the yield command, it looks exactly the same.

actuator

Generally, we encapsulate the code that executes the generator as a function and call this function that executes the generator code an executor. The CO module is a well-known executor.

A Generator is a container for asynchronous operations. Its automatic execution requires a mechanism that automatically returns execution when an asynchronous operation has a result. There are two ways to do this:

  1. Callback function. Wrap an asynchronous operation as a Thunk function and hand back execution in a callback function.
  2. Promise object. Wrap an asynchronous operation into a Promise object and hand back execution with the THEN method.

A simple automotor based on Promise objects:

function run(gen){
  var g = gen();

  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }

  next();
}
Copy the code

When we use it, we can just use it like this,


function* foo() {
    let response1 = yield fetch('https://xxx') // Return the promise object
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('https://xxx') // Return the promise object
    console.log('response2')
    console.log(response2)
}
run(foo);
Copy the code

In the above code, the next function automatically executes by calling itself as long as the Generator function has not executed the last step. By using generators and actuators, it is possible to write asynchronous code in a synchronous manner, which greatly improves the readability of the code.

async/await

The introduction of async/await in ES7 is a way to get rid of the executor and generator altogether and achieve more intuitive and concise code. According to the MDN definition, async is a function that executes asynchronously and implicitly returns a Promise as a result. Async is the syntax-sugar of Generator functions and is an improvement on Generator functions.

The preceding code, implemented with async, looks like this:

const foo = async() = > {let response1 = await fetch('https://xxx') 
    console.log('response1')
    console.log(response1)
    let response2 = await fetch('https://xxx') 
    console.log('response2')
    console.log(response2)
}
Copy the code

A comparison shows that async functions replace the asterisk (*) of Generator functions with async, yield with await, and nothing more.

The async function improves the Generator function in the following four aspects:

  1. Built-in actuator. Generator functions must rely on an actuator for execution, whereas async functions come with an actuator and do not need to manually execute the next() method.
  2. Better semantics. Async and await have clearer semantics than asterisks and yields. Async means that there are asynchronous operations in the function, and await means that the expression immediately following it needs to wait for the result.
  3. Wider applicability. The co module convention is that yield can only be followed by a Thunk function or a Promise object, whereas async’s await command can be followed by Promise objects and values of primitive types (numeric, string, and Boolean, etc.). This will automatically change to an immediately resolved Promise.
  4. The return value is Promise. The async function returns a Promise object, which is more convenient than the Iterator returned by the Generator function and can be called directly using the then() method.

The point here is that the built-in actuator encapsulates everything we need to do (write actuators/dependent CO modules) internally. Such as:

async function fn(args) {
  // ...
}
Copy the code

Is equal to:

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

function spawn(genF) { // The spawn function is the auto-executor, which is the same as the simple version, with more Promise and fault tolerance
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}
Copy the code

Async /await execution order

From the above analysis, we know that async implicitly returns a Promise as a result of the function, so the simple implication is that when the function following the await completes, the await will generate a microtask (promise.then is a microtask). However, we should pay attention to the timing of the microtask. It is generated by executing the await and immediately jumping out of the async function to execute other code (here is the coroutine operation, A pauses execution, B gives control). After the rest of the code is finished, return to the async function to execute the rest of the code and register the code following the await in the microtask queue. Let’s look at an example:

console.log('script start')

async function async1() {
await async2()
console.log('async1 end')}async function async2() {
console.log('async2 end')
}
async1()

setTimeout(function() {
console.log('setTimeout')},0)

new Promise(resolve= > {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')})console.log('script end')
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
Copy the code

Analyze this code:

  • Execute the code, outputscript start.
  • Async1 () is executed, async2() is called, and then outputasync2 endThe async1 function is left in context and the async1 function is skipped.
  • When setTimeout is encountered, a macro task is generated
  • Execute the Promise, outputPromise. When then is encountered, the first microtask is generated
  • Continue executing the code, outputscript end
  • After the code logic is executed (the current macro task is executed), the microtask queue generated by the current macro task is executed and outputpromise1When the microtask encounters then, a new microtask is generated
  • Execute the resulting microtask, outputpromise2, the current microtask queue is completed. Execute the power back to async1
  • Performing an await actually results in a promise return, i.e
let promise_ = new Promise((resolve,reject){ resolve(undefined)})
Copy the code

When the execution is complete, execute the statement after await with the output async1 end

  • Finally, the next macro task, which executes setTimeout, is executed to outputsetTimeout

Pay attention to

In the new version of Chrome, it is not printed as above, because Chrome has been optimized, and await is now faster. The output is:

// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
Copy the code

But this practice is actually against the specification, of course the specification can be changed, this is a PR of the V8 team, the new version of the print has been modified. Also has a discussion on zhihu, can take a look at www.zhihu.com/question/26…

We can understand it in two ways:

  1. If await is directly followed by a variable, such as: await 1; This is equivalent to directly registering the code following the await as a microtask, which can be simply read as promise.then(code below the await). When the promise function is encountered, the promise.then() function is registered to the microtask queue. Note that the microtask queue already contains the “await” microtask. So in this case, the code after the await (async1 end) is executed, followed by the microtask code registered after the async1 function (PromisE1, Promise2).

  2. If the await is followed by an asynchronous function call, as in the above code, change the code to look like this:

console.log('script start')

async function async1() {
await async2()
console.log('async1 end')}async function async2() {
console.log('async2 end')
return Promise.resolve().then((a)= >{
  console.log('async2 end1')
})
}
async1()

setTimeout(function() {
console.log('setTimeout')},0)

new Promise(resolve= > {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')})console.log('script end')
Copy the code

The output is:

// script start => async2 end => Promise => script end =>async2 end1 => promise1 => promise2 => async1 end => setTimeout
Copy the code

At this point, awit does not register the code following the await in the microtask queue. Instead, after executing the await, it jumps out of the async1 function and executes other code. Then when you come across a promise, register promise.then as a microtask. After the rest of the code is completed, we need to go back to the async1 function to execute the rest of the code and register the code following the await in the microtask queue. Note that there are previously registered microtasks in the microtask queue. In this case, a microtask outside of async1 will be executed first (promise1, Promise2), and then a microtask registered in async1 will be executed (async1 end).

The resources

  • Geek Time: How Browsers Work and Practice
  • Introduction to ES6 by Ruan Yifeng

Promise information

  • This Time, Understand the Promise Principle Thoroughly

The last

  • Welcome to add my wechat (Winty230), pull you into the technology group, long-term communication and learning…
  • Welcome to pay attention to “front end Q”, seriously learn front end, do a professional technical person…