Resolving function callbacks goes through several stages, from Promise objects to Generator functions to async functions. Async functions are by far the best solution for function callbacks. Async is implemented in many languages, including Python, Java Spring, Go and so on.

Use of async await

An async function returns a Promise object. When the function executes, it returns an await object, waits until the triggered asynchronous operation is complete, and then executes the following statement in the function body.

function getNum(num){
    return new Promise((resolve, reject) = > {
        setTimeout((a)= > {
            resolve(num+1)},1000)})}const func = async() = > {const f1 = await getNum(1)
  const f2 = await getNum(f1)
  console.log(f2) 
  3 / / output
}
func()
Copy the code

Async /await needs to be written outside the function async and await inside the function that needs to be executed

Deep understanding of

Understanding async requires understanding Generator functions, since async is the syntactic sugar of Generator functions.

[ˈ ˈ n ʃ ə n ʃ ə n] function – Generator

Generator is a new data type introduced by the ES6 standard. The Generator can be understood as a state machine that encapsulates many states internally and returns an Iterator object. This iterator allows you to iterate over the associated values and states. The salient feature of a Generator is that it can be returned multiple times, and each return value is saved as part of the iterator and can be invoked explicitly.

Declaration of a Generator function

Normal functions use the function declaration, return as the callback (no return is encountered, return undefined is called at the end), can only be called once. Generator functions are defined using function* and yield multiple times in addition to return.

function* foo(x) {
    yield x + 1;
    yield x + 2;
    return x + 3;
}
const result = foo(0) // foo {<suspended>}
result.next();  // {value: 1, done: false}
result.next();  // {value: 2, done: false}
result.next();  // {value: 3, done: true}
result.next();  //{value: undefined, done: true}
Copy the code

In the Chrome example, we see that foo returns an instance of a Generator. It has the state values SUSPENDED and closed, where SUSPENDED represents a pause and closed represents an end. However, this state cannot be captured, we can only obtain the current state through the method provided by the Generator function. After executing the next method, the return values of yield are executed sequentially. The returned value can be value or done. Value is the return value, which can be of any type. The done state can be false or true, and true indicates that the execution is complete. If {value: undefined, done: true} is used again after execution, it returns {value: undefined, done: true}. If {value: undefined, done: true} is used again after execution.

The method of the Generator function

The Generator function provides three methods, next/return/throw

The next approach is performed step by step, returning one value at a time, or passing in a new value each time as a calculation

function* foo(x) {
    let a = yield x + 1;
    let b= yield a + 2;
    return x + 3;
}
const result = foo(0) // foo {<suspended>}
result.next(1);  // {value: 1, done: false}
result.next(2);  // {value: 4, done: false}
result.next(3);  // {value: 3, done: true}
result.next(4);  //{value: undefined, done: true}
Copy the code

Return {value: undefined, done: true}

A throw returns the contents of a catch based on a try catch written in the function, or throws an exception if no try is written

function* foo(x) {
  try{
    yield x+1
    yield x+2
    yield x+3
    yield x+4
    
  }catch(e){
    console.log('catch it')}}const result = foo(0) // foo {<suspended>}
result.next();  // {value: 1, done: false}
result.next();  // {value: 2, done: false}
result.throw();  // catch it {value: undefined, done: true}
result.next();  //{value: undefined, done: true}
Copy the code

Here you can see that the state is executed sequentially before the throw is executed, but when the throw is encountered, it goes directly to the catche and changes the state.

Another thing to note here is that because the state machine is executed according to the steps used to execute the state, if a thow is executed without a try catch, it will throw an error

function* foo(x) {
    yield x+1
  try{
    yield x+2
   }catch(e){
    console.log('catch it')}}const result = foo(0) // foo {<suspended>}
result.next();  // {value: 1, done: false}
result.next();  // {value: 2, done: false}
result.throw();  // catch it {value: undefined, done: true}
result.next();  //{value: undefined, done: true}
Copy the code

This example has the same execution state as the previous one, because by the time the throw is executed, the try statement is already executed, so it can be executed, whereas the following example does not

function* foo(x) {
    yield x+1
  try{
    yield x+2
   }catch(e){
    console.log('catch it')}}const result = foo(0) // foo {<suspended>}
result.next();  // {value: 1, done: false}
result.throw();  // Uncaught undefined
result.next();  //{value: undefined, done: true}
Copy the code

When the throw is executed, it does not enter the try statement, so it throws an error. If the parameter is passed, it will be displayed as the parameter passed. This state is the same as when a try is not written.

traverse

The return value of the Generator function is an instance of a Generator with state. It can be called for of, traversed, and only called for of. All of his states are now returned

function* foo(x) {
   console.log('start')
    yield x+1
   console.log('state 1')
    yield x+2
   console.log('end')}const result = foo(0) // foo {<suspended>}
for(let i of result){
	console.log(i)
}
//start
/ / 1
//state 1
/ / 2
//end
result.next() //{value: undefined, done: true}
Copy the code

After calling the for of method, next() is called in the background, and when the done attribute is true, the loop exits. Thus instances of Generator functions are executed sequentially, and the state is completed when called again

Storage and change of state

Yield returns values in Generator functions that can be stored and changed by variables.

function* foo(x) {
    let a = yield x + 0;
    let b= yield a + 2;
    yield x;
    yield a 
    yield b
}
const result = foo(0)
result.next() // {value: 0, done: false}
result.next(2) // {value: 4, done: false}
result.next(3) // {value: 0, done: false}
result.next(4) // {value: 2, done: false}
result.next(5) // {value: 3, done: false}
Copy the code

In the second iteration, we pass in 2, the value of a in foo is replaced by 2, and on iteration 4, 2 is returned. In the third iteration, 3 is passed in, replacing the value 4 of B, and in the fifth iteration, 3 is returned. So the argument passed in is the value generated instead of the last iteration.

Yield * commissioned

In Generator functions, we sometimes need to combine the values of multiple iterators, and we can use yield * to delegate execution to another Generator function

function* foo1() {
    yield 1;
    yield 2;
    return "foo1 end";
}

function* foo2() {
    yield 3;
    yield 4;
}

function* foo() {
    yield* foo1();
    yield* foo2();
	  yield 5;
}

const result = foo();

console.log(iterator.next());// "{ value: 1, done: false }"
console.log(iterator.next());// "{ value: 2, done: false }"
console.log(iterator.next());// "{ value: 3, done: false }"
console.log(iterator.next());// "{ value: 4, done: false }"
console.log(iterator.next());// "{ value: 5, done: false }"
console.log(iterator.next());// "{ value: undefined, done: true }"
Copy the code

When foo executes, it first delegates to Foo1, and when foo1 completes, it delegates to Foo2. But we notice that “foo1 end” is not printed. A return can only occur once in the entire Generator, and all yield* is in the form of a function expression when delegating. The value of return is the result of the expression, which is paused internally until the delegate terminates, waiting for the result of the expression, which is returned directly to Foo. There are no received variables in foo at this point, so it is not printed. If we want to capture this value, we can use yield *foo() to get it.

Implement a simple async/await

Above, we have learned how to use Generator functions. The async/await syntax sugar operates using a Generator function + an automatic executor. We can refer to the following examples

// Defines a promise that simulates an asynchronous request by passing in the ++ argument
function getNum(num){
    return new Promise((resolve, reject) = > {
        setTimeout((a)= > {
            resolve(num+1)},1000)})}// The auto-executor is called recursively if a Generator function is not completed
function asyncFun(func){
  var gen = func();

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

  next();
}

// This is the Generator function that needs to be executed. The data inside will be called after the promise of one step has been fulfilled
var func = function* (){
  var f1 = yield getNum(1);
  var f2 = yield getNum(f1);
  console.log(f2) ;
};
asyncFun(func);
Copy the code

During execution, determine if a function’s promise is fulfilled, and if so, pass the result to the next function and repeat.

conclusion

Async /await is very easy to understand and can be described in a few sentences once you have a basic understanding of Generator functions. There is no further explanation of the internal execution logic and principles of Generator functions. If you have a deeper understanding of this, you are welcome to provide additional explanations.