Async/await

In multiple callback dependent scenarios, although Promise replaced callback nesting by chain invocation, excessive chain invocation is still not readable and the flow control is not convenient. The async function proposed in ES7 finally provides JS with the ultimate solution for asynchronous operation, which solves the above two problems in a concise and elegant way.

Imagine a scenario where there are dependencies between asynchronous tasks A -> B -> C, and if we handle these relationships through then chain calls, the readability is not very good.

If we want to control one of these processes, say, under certain conditions, B doesn’t go all the way down to C, then it’s not very easy to control either.

Promise.resolve(a)
  .then(b= > {
    // do something
  })
  .then(c= > {
    // do something
  })
  
Copy the code

But if this scenario is implemented with async/await, readability and flow control will be much easier.

async() = > {const a = await Promise.resolve(a);
  const b = await Promise.resolve(b);
  const c = await Promise.resolve(c);
}

Copy the code

So how do we implement an async/await? First we need to know that async/await is actually a wrapper around a Generator, which is a syntactic sugar.

As Generator has been replaced by async/await soon, many students are unfamiliar with Generator, so let’s take a look at its usage:

ES6 introduces Generator functions that suspend the execution flow of functions using the yield keyword and switch to the next state using the next() method, providing the possibility to change the execution flow and thus providing a solution for asynchronous programming.

function* myGenerator() {
  yield '1'
  yield '2'
  return '3'
}

const gen = myGenerator();  // Get the iterator
gen.next()  //{value: "1", done: false}
gen.next()  //{value: "2", done: false}
gen.next()  //{value: "3", done: true}

Copy the code

Yield can also be given a return value by passing an argument to next()

function* myGenerator() {
  console.log(yield '1')  //test1
  console.log(yield '2')  //test2
  console.log(yield '3')  //test3
}

// Get the iterator
const gen = myGenerator();

gen.next()
gen.next('test1')
gen.next('test2')
gen.next('test3')

Copy the code

️ should be familiar with the use of Generator. */yield and async/await look very similar in that they both provide the ability to pause execution, but there are three differences:

  • Async /await comes with an executor that automatically executes the next step without manually calling next()

  • Async functions return a Promise object, while Generator returns a Generator object

  • Await can return the resolve/reject value of a Promise

Our implementation of async/await corresponds to the above three encapsulation generators.

automated

Let’s take a look at the process of manual execution for such a Generator.

function* myGenerator() {
  yield Promise.resolve(1);
  yield Promise.resolve(2);
  yield Promise.resolve(3);
}

// Execute iterators manually
const gen = myGenerator()
gen.next().value.then(val= > {
  console.log(val)
  gen.next().value.then(val= > {
    console.log(val)
    gen.next().value.then(val= > {
      console.log(val)
    })
  })
})

// Output 1, 2, 3

Copy the code

Yield can also return resolve by passing a value to Gen.next ().

function* myGenerator() {
  console.log(yield Promise.resolve(1))   / / 1
  console.log(yield Promise.resolve(2))   / / 2
  console.log(yield Promise.resolve(3))   / / 3
}

// Execute iterators manually
const gen = myGenerator()
gen.next().value.then(val= > {
  // console.log(val)
  gen.next(val).value.then(val= > {
    // console.log(val)
    gen.next(val).value.then(val= > {
      // console.log(val)
      gen.next(val)
    })
  })
})
Copy the code

Obviously, manual execution looks awkward and ugly, and we want the generator function to execute automatically, with yield returning the value of resolve.

Based on these two requirements, we make a basic encapsulation, where async/await is the keyword and cannot be overridden, we simulate with functions:

function run(gen) {
  var g = gen()                     // Since gen() gets the latest iterator each time, the iterator must be fetched before _next(), otherwise an infinite loop will occur

  function _next(val) {             // Encapsulate a method that recursively executes g.ext ()
    var res = g.next(val)           // Get the iterator object and return the value of resolve
    if(res.done) return res.value   // Recursive termination condition
    res.value.then(val= > {         //Promise's then method is a prerequisite for automatic iteration
      _next(val)                    // Wait for the Promise to complete and automatically execute the next, passing in the value of resolve
    })
  }
  _next()  // This is the first execution
}

Copy the code

For our previous example, we could do this:

function* myGenerator() {
  console.log(yield Promise.resolve(1))   / / 1
  console.log(yield Promise.resolve(2))   / / 2
  console.log(yield Promise.resolve(3))   / / 3
}

run(myGenerator)

Copy the code

This gives us an initial implementation of async/await.

The above code is only five or six lines long, but it is not easy to understand. We have used four examples to help readers understand the code better.

In simple terms, we encapsulate a run method that encapsulates the next operation as _next(), and executes _next() every time promise.then () to achieve automatic iteration.

During the iteration, we also pass the value of resolve to gen.next(), allowing yield to return the Promise’s value of resolve

By the way, is it only the.then method that does what we do automatically? The answer is no. In addition to Promise, yield can be followed by thunk functions. Thunk functions are not new.

The core of both the Promise and thunk functions is the automatic execution of the Generator by passing in callbacks. Thunk function only as an extension of knowledge, students who have difficulty in understanding can also skip this, does not affect the subsequent understanding.

Return Promise & exception handling

While we have implemented automatic execution of Generator and yield returning resolve, there are a few problems with the above code:

  • Basic type compatibility: This code executes automatically only if yield is followed by a Promise. To be compatible with basic type values, yield and Promise (gen().next-value) should be converted to promise.resolve ()

  • Lack of error handling: the Promise in the code above, failure will lead to subsequent handling directly, we need through a call to the Generator. The prototype. Throw (), throw an error, can be the outer try-catch captured

  • The return value is Promise: the return value of async/await is a Promise, and we need to be consistent here by giving the return value package a Promise

Let’s modify the run method:

function run(gen) {
  // Wrap the return value as a promise
  return new Promise((resolve, reject) = > {
    var g = gen()

    function _next(val) {
      // Error handling
      try {
        var res = g.next(val) 
      } catch(err) {
        return reject(err); 
      }
      if(res.done) {
        return resolve(res.value);
      }
      //res.value is wrapped as a promise to accommodate cases where yield is followed by a basic type
      Promise.resolve(res.value).then(
        val= > {
          _next(val);
        }, 
        err => {
          // Throw an error
          g.throw(err)
        });
    }
    _next();
  });
}

Copy the code

Then we can test it:

function* myGenerator() {
  try {
    console.log(yield Promise.resolve(1)) 
    console.log(yield 2)   / / 2
    console.log(yield Promise.reject('error'))}catch (error) {
    console.log(error)
  }
}

const result = run(myGenerator)     //result is a Promise
// Output 1, 2 error

Copy the code

At this point, an async/await implementation is almost complete. Async /await = async/await = async/await = async/await = async/await

// equivalent to our run()
function _asyncToGenerator(fn) {
  // return a function, consistent with async. Our run executes Generator directly, which is not quite canonical
  return function() {
    var self = this
    var args = arguments
    return new Promise(function(resolve, reject) {
      var gen = fn.apply(self, args);

      // equivalent to our _next()
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
      }
      // Handle exceptions
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
      }
      _next(undefined);
    });
  };
}

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(_next, _throw); }}Copy the code

Usage:

const foo = _asyncToGenerator(function* () {
  try {
    console.log(yield Promise.resolve(1))   / / 1
    console.log(yield 2)                    / / 2
    return '3'
  } catch (error) {
    console.log(error)
  }
})

foo().then(res= > {
  console.log(res)                          / / 3
})

Copy the code

So much for the async/await implementation. However, until the end, we do not know exactly how await suspends, and the secret of await suspends is to find out in the implementation of Generator.

The Generator to realize

Starting with a simple Generator example, we will explore the implementation principle of Generator step by step:

function* foo() {
  yield 'result1'
  yield 'result2'
  yield 'result3'
}
  
const gen = foo()
console.log(gen.next().value)
console.log(gen.next().value)
console.log(gen.next().value)

Copy the code

We can translate this code online at Babel to see how Generator is implemented in ES5:

"use strict";

var _marked =
/*#__PURE__*/
regeneratorRuntime.mark(foo);

function foo() {
  return regeneratorRuntime.wrap(function foo$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 'result1';

        case 2:
          _context.next = 4;
          return 'result2';

        case 4:
          _context.next = 6;
          return 'result3';

        case 6:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}

var gen = foo();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);

Copy the code

The code doesn’t look very long at first glance, but if you look closely you’ll notice that there are two things you don’t recognize: RegeneratorRuntime. mark and RegeneratorRuntime. wrap, which are two methods in the Regenerator-Runtime module.

The regenerator module comes from the Facebook Regenerator module. The complete code is in runtime.js. This Runtime has more than 700 lines… -_ – | |, so we can’t speak, don’t too important part of the us is simply too, focuses on suspended related part of the code.

I think the effect of gnawing source code is not very good, suggest that readers pull to the end of the first look at the conclusion and brief version of the implementation, source code as a supplementary understanding.

regeneratorRuntime.mark()

Regeneratorruntime.mark (foo) is called on the first line, so let’s look at the definition of the mark() method in runtime.

// Runtime.js has a slightly different definition, with a bit more judgment. Here is the compiled code
runtime.mark = function(genFun) {
  genFun.__proto__ = GeneratorFunctionPrototype;
  genFun.prototype = Object.create(Gp);
  return genFun;
};

Copy the code

Edge GeneratorFunctionPrototype and Gp here we don’t know, they are defined in the runtime, but it doesn’t matter, as long as we know, mark () method for generator function (foo) binding a series of the prototype is ok, here is simply.

regeneratorRuntime.wrap()

What does this method do? What does it want to wrap? Let’s look at the definition of the wrap method:

// Runtime.js has a slightly different definition, with a bit more judgment. Here is the compiled code
function wrap(innerFn, outerFn, self) {
  var generator = Object.create(outerFn.prototype);
  var context = new Context([]);
  generator._invoke = makeInvokeMethod(innerFn, self, context);

  return generator;
}

Copy the code

The wrap method creates a generator and inherits outerFn. Prototype; And then new a context object; The makeInvokeMethod method receives innerFn(for foo$), context, and this and attaches the return value to generator._invoke; Finally return generator.

Wrap () essentially adds a _invoke method to the generator.

OuterFn. Prototype, Context, makeInvokeMethod, makeInvokeMethod Let’s answer them one by one:

OuterFn. Prototype is actually genFun. Prototype

We can see this by combining the above code

Context can be understood directly as a global object used to store various states and contexts:

var ContinueSentinel = {};

var context = {
  done: false.method: "next".next: 0.prev: 0.abrupt: function(type, arg) {
    var record = {};
    record.type = type;
    record.arg = arg;

    return this.complete(record);
  },
  complete: function(record, afterLoc) {
    if (record.type === "return") {
      this.rval = this.arg = record.arg;
      this.method = "return";
      this.next = "end";
    }

    return ContinueSentinel;
  },
  stop: function() {
    this.done = true;
    return this.rval; }};Copy the code

MakeInvokeMethod is defined as follows. It returns an invoke method that determines the current state and executes the next step, which is actually the next() we call

// Here is the compiled code
function makeInvokeMethod(innerFn, context) {
  // Set the state to start
  var state = "start";

  return function invoke(method, arg) {
    / / has been completed
    if (state === "completed") {
      return { value: undefined.done: true };
    }
    
    context.method = method;
    context.arg = arg;

    / / implementation
    while (true) {
      state = "executing";

      var record = {
        type: "normal".arg: innerFn.call(self, context)    // Go to the next step and get the status.
      };

      if (record.type === "normal") {
        // Determine whether the execution is complete
        state = context.done ? "completed" : "yield";

        // ContinueSentinel is an empty object,record.arg === {} then skip return to the next loop
        // When record-. arg is null, the answer is either no yield statement or a return has been made, i.e. when the switch returns a null value.
        if (record.arg === ContinueSentinel) {
          continue;
        }
        // Return value of next()
        return {
          value: record.arg,
          done: context.done }; }}}; }Copy the code

Why is generator._invoke actually gen. Next when the Runtime defines next(), next() actually returns the _invoke method

// Helper for defining the .next, .throw, and .return methods of the
// Iterator interface in terms of a single ._invoke method.
function defineIteratorMethods(prototype) {["next"."throw"."return"].forEach(function(method) {
      prototype[method] = function(arg) {
        return this._invoke(method, arg);
      };
    });
}

defineIteratorMethods(Gp);

Copy the code

Low profile implementation & call flow analysis

After all, there are many concepts and packages in the source code. It will not be easy to fully understand. Let’s jump out of the source code and implement a simple Generator, and then look back at the source code.

// The generator function splits the code into switch-case blocks based on the yield statement, and then executes each case separately by toggling _context.prev and _context.next
function gen$(_context) {
  while (1) {
    switch (_context.prev = _context.next) {
      case 0:
        _context.next = 2;
        return 'result1';

      case 2:
        _context.next = 4;
        return 'result2';

      case 4:
        _context.next = 6;
        return 'result3';

      case 6:
      case "end":
        return_context.stop(); }}}// Lower version context
var context = {
  next:0.prev: 0.done: false.stop: function stop () {
    this.done = true}}// Invoke with low configuration
let gen = function() {
  return {
    next: function() {
      value = context.done ? undefined: gen$(context)
      done = context.done
      return {
        value,
        done
      }
    }
  }
} 

// Test use
var g = gen() 
g.next()  // {value: "result1", done: false}
g.next()  // {value: "result2", done: false}
g.next()  // {value: "result3", done: false}
g.next()  // {value: undefined, done: true}

Copy the code

This code is not difficult to understand, let’s examine the call flow:

  • The function* generator function we defined is converted to the above code

  • The transformed code is divided into three chunks:

  1. Gen $(_context) is derived from the yield split generator function code

  2. The context object is used to store the execution context of a function

  3. The invoke() method defines next(), which executes gen$(_context) to skip to the next step

  • When we call g.ext (), we invoke the invoke() method, execute gen$(_context), and enter the switch statement, which executes the corresponding case block according to the context identifier and returns the corresponding result

  • G.ext () returns {value: undefined, done: true} when the generator function runs to the end (no next yield or return) and the switch does not match the corresponding code block.

The function is not actually suspended. Each yield actually executes the incoming Generator function, but a context object is used to store the context in the middle of the process, so that each time a Generator function is executed, Can be executed from the last execution result, as if the function was suspended.

This work is reproduced (read the original text)