Scene of an interview:

Interviewer: Do you know async/await?

I: have some understanding (in the heart secretly happy, it seems that the following to ask me the event cycle aspect of the thing, immediately give you back out, steady very)

Interviewer: How does Bable handle async/await? Or just describe how polyfill works

Me:… (Why don’t you play by the rules?)

I really don’t know this stuff, but in order to avoid embarrassment, I can only carry on although I don’t know what you said this thing but momentum can’t weak psychological tactics, must make you live with what they know, on-site fortune-telling speculate that affectionately introduced a wave of asynchronous function queue execution mode, unfortunately, I said it like it was flying, but I didn’t get it right

I have nothing to do recently, so I took the time to have a look

Polyfill the code

To see how this works, go to the REPL online editor on Babel’s website, configure the presets and plugins, type in the code you want to convert, and Babel will automatically output the converted code to you

Take the following code for example:

async function test1 () {
  console.log(111)
  await a()
  console.log(222)
  await b()
  console.log(3)}Copy the code

Babel outputs the following code:

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));

function test1() {
  return _test.apply(this.arguments);
}

function _test() {
  _test = (0, _asyncToGenerator2.default)(
  /*#__PURE__*/
  _regenerator.default.mark(function _callee() {
    return _regenerator.default.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            console.log(111);
            _context.next = 3;
            return a();

          case 3:
            console.log(222);
            _context.next = 6;
            return b();

          case 6:
            console.log(3);

          case 7:
          case "end":
            return _context.stop();
        }
      }
    }, _callee);
  }));
  return _test.apply(this.arguments);
}
Copy the code

Obviously, the contents of the body of the while(1) method in the _test function are the first code to notice

As can be seen, Babel divides the original code once, dividing the code in the async function into each case of switch according to “await” as the boundary (for convenience of expression, contents in this case code block will be called “await” code block below). The switch condition is _context.prev = _context.next, which is closely related to _context.next, and the _context.next variable, which is assigned in every non-case end, The value is the content of the next await block to be executed in the original code after being partitioned. After all await is executed in the original code, the case end logic will be entered and return _context.stop() will be executed, indicating that the async function has been completed

But that’s just the basics, how does the code actually string together and keep looking out

Version: “@babel/ Runtime “: “7.8.4”

Process connections

First, take a look at the _interopRequireDefault method:

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : {
    "default": obj
  };
}

module.exports = _interopRequireDefault;
Copy the code

If obj has an __esModule attribute, return obj. If obj has an __esModule attribute, return obj. If obj has an __esModule attribute, return obj. Make sure that the current reference must have a default attribute, otherwise add a default attribute to it, so that the module default will not be undefined, is a simple tool method

And then continue to see _regenerator, while (1) in the loop body function, as _regenerator. Default. Wrap method of parameters is carried out, from @ _regenerator Babel/runtime/regenerator, the introduction of Enter @ Babel/runtime/regenerator file, there is only one line of code: module. Exports = the require (” regenerator – runtime “); , so the regenerator-Runtime library should end up with the wrap method

function wrap(innerFn, outerFn, self, tryLocsList) {
  // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator.
  var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator;
  var generator = Object.create(protoGenerator.prototype);
  var context = new Context(tryLocsList || []);

  // The ._invoke method unifies the implementations of the .next,
  // .throw, and .return methods.
  generator._invoke = makeInvokeMethod(innerFn, self, context);

  return generator;
}
Copy the code

InnerFn is _callee$, outerFn is _callee, outerFn. Prototype is _callee. Prototype, _callee is also a function, But after the _regenerator.default.mark method, take a look at the mark method

exports.mark = function(genFun) {
  if (Object.setPrototypeOf) {
    Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);
  } else {
    genFun.__proto__ = GeneratorFunctionPrototype;
    if(! (toStringTagSymbolin genFun)) {
      genFun[toStringTagSymbol] = "GeneratorFunction";
    }
  }
  genFun.prototype = Object.create(Gp);
  return genFun;
};
Copy the code

Is mainly in order to construct the prototype chain, GeneratorFunctionPrototype and Gp?

function Generator() {}
function GeneratorFunction() {}
function GeneratorFunctionPrototype() {}
// ...
var IteratorPrototype = {};
/ /...
var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype);
Copy the code

Again, build the prototype chain, which ends as follows:

So, going back to the wrap method above, protoGenerator is outerFn, _callee, and the prototype chain of the generator points to protoGenerator. Prototype has a context instance, Constructed from Context in:

function Context(tryLocsList) {
  // The root entry object (effectively a try statement without a catch
  // or a finally block) gives us a place to store values thrown from
  // locations where there is no enclosing try statement.
  this.tryEntries = [{ tryLoc: "root" }];
  tryLocsList.forEach(pushTryEntry, this);
  this.reset(true);
}
Copy the code

Reset method:

/ /...
Context.prototype = {
  constructor: Context,
  reset: function(skipTempReset) {
    this.prev = 0;
    this.next = 0;
    // Resetting context._sent for legacy support of Babel's
    // function.sent implementation.
    this.sent = this._sent = undefined;
    this.done = false;
    this.delegate = null;

    this.method = "next";
    this.arg = undefined;
    / /...
  },
  / /...
}
Copy the code

The reset method, like its property name, is used to initialize several properties. The main properties are this.prev and this.next, which are used to alternately record which code blocks are currently executed, and this.done, which is used to indicate whether or not the current code block is executed

A _invoke method is then mounted on the generator

// The ._invoke method unifies the implementations of the .next,
// .throw, and .return methods.
generator._invoke = makeInvokeMethod(innerFn, self, context);
Copy the code

Look at the code for makeInvokeMethod:

function makeInvokeMethod(innerFn, self, context) {
  var state = GenStateSuspendedStart;
  return function invoke(method, arg) {
    / /...}}Copy the code

At a glance, this method returns a method. What is inside the method body

_regenerator.default.mark(function _callee() {//… }) as _asyncToGenerator2. The parameters of the default method execution, so keep watching _asyncToGenerator2:

function _asyncToGenerator(fn) {
  return function () {
    var self = this,
        args = arguments;
    return new Promise(function (resolve, reject) {
      var gen = fn.apply(self, args);

      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }

      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }

      _next(undefined);
    });
  };
}
Copy the code

_asyncToGenerator also returns a function that returns a Promise. The async function also returns a Promise, calling asyncGeneratorStep via _next:

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

The gen parameter is the generator mentioned above. Normally, the key is “next”, gen[key](arg); Equivalent to generator. Next (arg), where does generator get the next attribute? In fact, the prototype chain is used to find Gp, where the next property exists:

// 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

This._invoke (method, arg) for this; _invoke(“next”, ARG)

So, now look again at the invoke method returned by the makeInvokeMethod method. Normal logic would follow this code:

function tryCatch(fn, obj, arg) {
  try {
    return { type: "normal".arg: fn.call(obj, arg) };
  } catch (err) {
    return { type: "throw".arg: err }; }}/ /...
var record = tryCatch(innerFn, self, context);
if (record.type === "normal") {
  // If an exception is thrown from innerFn, we leave state ===
  // GenStateExecuting and loop back for another invocation.
  state = context.done
    ? GenStateCompleted
    : GenStateSuspendedYield;

  if (record.arg === ContinueSentinel) {
    continue;
  }
  return {
    value: record.arg,
    done: context.done
  };
}
Copy the code

The tryCatch method returns an object with two value, done attributes. The first argument to tryCatch, fn, is the _callee$method that contains the while(1) fragment

Process analytical

In the body of the while(1), the _context argument is an instance of Context. As mentioned above, the prev and next properties on _context are initialized to 0, so case 0 is entered, the first await block is executed, and the info result is obtained. Determine the value of info.done

if (info.done) {
  resolve(value);
} else {
  Promise.resolve(value).then(_next, _throw);
}
Copy the code

Here is the logic to ensure that all await bodies in the original async function are completed

If info.done is not true, the body of await code in the original async function is not fully executed. Enter the else statement, use promise. resolve to wait for the Promise state of the current await block to change, and then call the then method. Call asyncGeneratorStep by executing the _next method, continue with _callee$, go through the switch section again, proceed to the next case according to the updated _context_prev, and so on. After all await blocks are finished, case ‘end’ is entered and _context.stop() is executed; This thing

Context.prototype = {
  constructor: Context,
  / /...
  stop: function() {
    this.done = true;

    var rootEntry = this.tryEntries[0];
    var rootRecord = rootEntry.completion;
    if (rootRecord.type === "throw") {
      throw rootRecord.arg;
    }
    return this.rval;
  },
  / /...
}
Copy the code

The stop method sets this. Done to true, indicating that the current asyncGeneratorStep is completed. The next asyncGeneratorStep is executed.

if (info.done) {
  resolve(value);
}
Copy the code

The _next call is not continued and the process ends

In fact, during the interview, the interviewer asked me the realization principle of async/await. My first reaction was Promise, but then I remembered that Promise belongs to ES6, and polyfill should be ES5 at least, so I gave up the idea. You can also double polyfill

Simplified version implementation

From the above analysis, it can be seen that Babel’s polyfill of async/await is basically Promise + self-calling function. Of course, the premise is that the async function is segmented according to await point by string parser. The string parser usually uses the @babel/parser/@babel/ Generator /@babel/traverse series, but this is not the focus of this article, so I won’t cover it

Suppose you have implemented a parser that splits the incoming async function into parts as required

For example, for the following source code:

// Wait () is a function that returns promise
async function test1 () {
  console.log(111)
  await wait(500)
  console.log(222)
  await wait(1000)
  console.log(333)}Copy the code

Will be converted to:

function test1 () {
  this.prev = 0
  return new Promise(resolve= > {
    function loop(value, _next) {
      return Promise.resolve(value).then(_next)
    }
    function fn1 () {
      switch (this.prev) {
        case 0:
          console.log(111);
          this.prev = 3;
          return loop(wait(500), fn1);
        case 3:
          console.log(222);
          this.prev = 6;
          return loop(wait(1000), fn1);
        case 6:
          console.log(333);
          return resolve()
      }
    }
    fn1(resolve)
  })
}
Copy the code

Of course, this is just a simple implementation, many things are not considered, such as await return value, function return value, etc., just to show the principle

The for loop?

At that time, the interview, when I had finished reel asynchronous function of queue execution mode The concept, the interviewer may have never thought I’d in Ming knew that they were in the dark can also mentality is so good to say so much, quiet for a moment, seems to be to suppress my arrogance arrogance, ask again, if it is a for loop, how to deal with?

Code similar to the following:

async function fn1 () {
  for (let i = 0; i < 10; i++) {
    await wait(i * 100)}}Copy the code

At that time I actually already know guess wrong, but since guess that guess after all, oneself install of force in any case also want circle to come back, then continue to explain with this concept forcibly 1

I actually had the right idea when I did this for loop, which was to take the for loop apart and get a single expression; Conditional expression; Babel processes these three expressions, and then changes the conditional expression until the final loop body is triggered:

// Just look at the body code
switch (_context.prev = _context.next) {
  case 0:
    i = 0;

  case 1:
    if(! (i <10)) {
      _context.next = 7;
      break;
    }

    _context.next = 4;
    return wait(i * 100);

  case 4:
    i++;
    _context.next = 1;
    break;

  case 7:
  case "end":
    return _context.stop();
}
Copy the code

This reveals one feature of the async/await function, which is its ability to suspend for loops, i.e., for loops

Generator?

Having seen the implementation of async/await, take a look at the Generator for the following code:

function* generatorFn() {
  console.log(111)
  yield wait(500)
  console.log(222)
  yield wait(1000)
  console.log(333)}Copy the code

Babel translates this into:

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));

var _marked =
/*#__PURE__*/
_regenerator.default.mark(generatorFn);

function generatorFn() {
  return _regenerator.default.wrap(function generatorFn$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          console.log(111);
          _context.next = 3;
          return wait(500);

        case 3:
          console.log(222);
          _context.next = 6;
          return wait(1000);

        case 6:
          console.log(333);

        case 7:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}
Copy the code

This is the same as async/await, with the exception that the Generator is shred by the yield keyword. The main difference is that the transformed code is not async/await. Missing the _asyncToGenerator2 method call, which is used for self-invocation execution and is the difference between async/await and Generator

In the async function, as long as the value returned by the expression after await is a non-promise or a fulfilled Promise, the async function will automatically continue to execute, which is represented as a self-calling method in polyfill

For Generator functions to continue after yield, they must manually call the next method externally, which is actually called automatically by _asyncToGenerator2 in the polyfill of async/await

In addition, since it is a manual call, the Generator itself will not wait for the promise state to change if you do not add additional processing for asynchronous promises. Async /await is the synsyntax sugar of the Generator. Part of the reason is that async/await has built-in processing for asynchronous promises compared to Generator

summary

I’ve been in a few interviews lately, and I’ve noticed that interviewers love to ask you what your highlights are, both business and technical, and follow up on your answers to see how good they really are

For example, if you are familiar with VUE and React is used in his team, it may be difficult for him to get the result after asking you React. On the other hand, You can’t guarantee that you’ll look your best in every interview. What if you’re not even on the same channel as the interviewer and you can’t understand each other? So giving you the choice, giving you the opportunity to choose for yourself, which begs the question, what if you haven’t really done anything interesting? It’s nobody’s fault you couldn’t take the chance you were given

So, if you have a higher pursuit, then in the usual work, even write business code every day, you will have your own thinking, this component can change a kind of writing, if the demand can simplify the webpack need to upgrade to the latest version of the project, this problem can you build a wheel to fix it once and for all?

Irrelevant problem size, can cause thinking, in fact, under normal circumstances is unlikely to have much of a problem for you to solve, in most cases are small problems, but the problem again small, much to solve it is also a substantial accumulation, through the accumulation, within the team, you have to take out for the output of the contribution, leaves the group, That’s how you can take advantage of the opportunities the interviewer offers you

Sometimes, it’s better than your back-swiping algorithm — after all, an interview question or algorithm will be a yes and a no, but there’s no right answer to a bright spot. There’s a lot you can say about it