Original address: Regenerator

I searched around for the Generator and talked mostly about its usage, but very little about how it works, namely “coroutines”. But because it’s something we use every day, directly or indirectly, we’re going to write an article about it

History of JS callback

A,Callback

  1. In ES5 and earlier, writing callbacks was mostly callback, so leave callback hell alone

Second, the Promise

  1. Promise optimizes the way callbacks are written by chain-calling, which is essentially a callback. Deferred encapsulated by it can also be seen in various open source libraries, such as Qiankun

  2. Promises themselves are nothing new, but callbacks registered by THEN perform this during the microtask phase of the current event loop, meaning promises can only be provided at the native level. User-level polyfill can only be done with macro tasks, such as promise-polyfill

Third, the Generator

  1. Generator is the star of this article. ES6’s blockbuster feature, known as a state machine, contains various states and uses yield to trigger the next step

  2. Generator introduces the concept of “coroutines” that traditional callbacks cannot match, which means that asynchronous code can be written synchronously with automatic execution, such as tJ’s CO library

  3. The generator object also implements:

  • Iterable protocol (symbol. iterator) : available through for… Of iterates, such as the built-in objects Array and String, which implement this protocol

  • Iterator protocol (next()) : Its next method can be called to get {value: any, done: Boolean} to determine the status

Async, await

  1. Generator, yield syntax sugar, select some features. The flip side of that is that you cut off some functionality (more on that later)

  2. If Babel is used to compile code with async, await, and yield, then asyncGeneratorStep and _asyncToGenerator are automatically executed

  3. The principle is simple:

  • Get the Generator object and use Promise’s microtask capabilities to execute next

  • The value returned by ret.value is the value of await, encapsulated as a Promise for the next entry

  • Judge each recursion until done is returned true

async function a() {}

function* b() {}

// Babel is compiled
function asyncGeneratorStep(gen, resolve, reject, _next, ...) {
  // Call gen's next or throw method
  var info = gen[key](arg);
  var value = info.value;

  if (info.done) {
    resolve(value);
  } else {
    // Execute recursively
    Promise.resolve(value).then(_next, _throw); }}function _asyncToGenerator(fn) {
  return function () {
    return new Promise(function (resolve, reject) {
      // Get the generator object
      var gen = fn.apply(self, arguments);
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }
      // Initialize next
      _next(undefined);
    });
  };
}
Copy the code

Generator Object, Generator, GeneratorFunction

A, the generator Object

  1. It is executed by Generator and returned with prototype methods such as next, return, and throw
function* gen() {}
const gObj = gen();

gObj.next();
gObj.return();
Copy the code

Second, the Generator

  1. throughfunction*Syntax, which is an instance of GeneratorFunction
Object.getPrototypeOf(gen).constructor // GeneratorFunction {prototype: Generator, ... }
Copy the code
  1. The Generator function itself has little meaning at the user code level and is rarely used

Third, GeneratorFunction

  1. It is a built-in function, but it is not attached directly to the window, but we can get it from an instance of it
const GeneratorFunction = Object.getPrototypeOf(gen).constructor;
Copy the code
  1. GeneratorFunction andFunctionIs a level that can be passed to create functions such as
const gen = new GeneratorFunction('a'.'yield a * 2');
const gObj = gen(10);
gObj.next().value / / 20
Copy the code

Working principles of the Generator

Examples of code for the positive start:


let num = 0;
async function gen() {
  num = num + (await wait(10));
  await 123;
  await foo();
  return num;
}

function wait(num: number) {
  return new Promise((resolve) = > setTimeout(() = > resolve(num), 600));
}

async function foo() {
  await "literal";
}

await gen();
console.log("regenerator: res", num);
Copy the code

First, the core point

  1. How is the state of the Generator implemented, or how does the Generator stop at yield

  2. How do multiple generators collaborate, i.e. give authority to one Generator and then give authority back

  3. How one Generator listens for the execution of another Generator, yield* genFn()

Ii. The relationship between Generator, GeneratorFunction and its prototype

If you’ve forgotten about prototype chains and inheritance, I suggest you take a look at prototype&extends

class GeneratorFunction {}

// GeneratorFunction prototype
class GeneratorFunctionPrototype {
  static [Symbol.toStringTag] = "GeneratorFunction";

  // Implement iterator Protocol
  next(args) {}

  return(args) {}

  throw(args) {}

  // Implement iterable Protocol
  [Symbol.iterator]() {
    return this; }}// Reference each other
GeneratorFunctionPrototype.constructor = GeneratorFunction;
GeneratorFunction.prototype = GeneratorFunctionPrototype;

// Set prototype
class Generator {}
Generator.prototype = GeneratorFunctionPrototype.prototype;
Copy the code

Ii. State of the Generator

  1. It is not difficult to implement the state machine. The state is recorded through a flag, and the next state is recorded after each state operation, and the execution is carried out after a certain time

  2. The state machine is generated by user-level code, which uses switch case + Context to record parameters

function _callee$(_context) {
  while (1) {
    switch (_context.next) {
      case 0:
        // await wait(10)
        _context.next = 3;
        return wait(10);
      case 3:
        // await 123
        _context.next = 7;
        return 123;
      case 7:
        _context.next = 9;
        // await foo()
        return foo();
      case "end":
        return_context.stop(); }}}Copy the code
  1. Each yield corresponds to a switch case, which returns each time.

Three, multiple Generator collaboration

  1. Case return Indicates that the Generator is granted permission to actively execute another Generator and exit its own state

  2. If foo Generator is a switch case, how does it revert to the parent state machine and trigger the parent state machine to continue

  3. Let’s see how Babel compiles async functions. Leaving mark and warp aside, _asyncToGenerator is automatic execution as we said earlier, which is the same as co(markFn). On the other hand, you can infer that the regeneratorRuntime.mark function returns a Generator for polyfill

function _foo() {
  _foo = _asyncToGenerator(
    regeneratorRuntime.mark(function _callee2() {
      return regeneratorRuntime.wrap(function _callee2$(_context2) {
        switch (_context2.next) {
          case 0:
            _context2.next = 2;
            return "literal";
          case "end":
            return_context2.stop(); } }, _callee2); }));return _foo.apply(this.arguments);
}
Copy the code
  1. {value: “literal”, done: True} is returned by the mark function and given to _asyncToGenerator. How is this used? Promise.then (next)

  2. What about collaboration? Don’t limit yourself to foo, the parent gen function does the same thing! The gen function waits for Foo resolve, of course, then gen returns {value: fooRetValue, done: false}, and continues next

  3. Clean up:

  • The parent gen function executes a case, takes the return value of the child foo as the result of the case, and then blocks itself (i.e. waiting for the child Promise resolve).

  • Foo returns done true after executing, terminates his state career, and then resolves his CO Promise resolve

  • (3) Gen stuck Promise receives foo’s result, returns done false this time, starts next, and re-enters the corresponding case via context.next

  1. So as you can see, Generator is not going to be a big deal without Promise, either native or polyfill, mainly because, again, we can’t interfere with v8’s event loops at the JS level

Mark, wrap, Context

  1. You should know the MARK function: Take a function and transform it into a Generator. How do you do that? Inherit
function mark(genFn: () => void) {
  return _inheritsLoose(genFn, GeneratorFunctionPrototype);
}

function _inheritsLoose(subClass, superClass) {
  Object.setPrototypeOf(subClass, superClass);
  subClass.prototype = Object.create(superClass.prototype);
  subClass.prototype.constructor = subClass;
  return subClass;
}
Copy the code
  1. Each wrap creates a context to manage state and context parameters, and a snapshot is taken each time the case is executed to prevent the parameters from changing after yield

  2. The mark function’s next, return, and throw calls are the wrap’s ability to coordinate the user code (switch case) with the context to decide what to do next. So to perfect the GeneratorFunctionPrototype, connects and wrap it, only in charge transfer type and args

type GeneratorMethod = "next" | "return" | "throw";

class GeneratorFunctionPrototype {
  // set by wrap fn
  private _invoke: (method: GeneratorMethod, args) = > { value: any.done: boolean };

  // Note that this is a prototype method
  next(args) {
    return this._invoke("next", args);
  }

  return(args) {
    return this._invoke("return", args);
  }

  throw(args) {
    return this._invoke("throw", args); }}Copy the code
  1. Wrap implementation
function wrap(serviceFn) {
  / / still borrow GeneratorFunctionPrototype ability
  const generator = new Generator();
  const context = new Context();

  let state = GenStateSuspendedStart;
  / / implementation _invoke
  generator._invoke = function invoke(method: GeneratorMethod, args) {
    context.method = method;
    context.args = args;

    if (method === "next") {
      // Record context parameters
      context.sent = args;
    } else if (method === "throw") {
      throw args
    } else {
      context.abrupt("return", args);
    }

    // Execute business code
    const value = serviceFn(context);
    state = context.done ? GenStateCompleted : GenStateSuspendedYield;

    return {
      value,
      done: context.done
    };
  };

  return generator;
}
Copy the code
  1. Context records the current running status and Context parameters, and provides termination, error reporting, and proxy methods
class Context {
  next: number | string = 0;
  sent: any = undefined;
  method: GeneratorMethod = "next";
  args: any = undefined;
  done: boolean = false;
  value: any = undefined;

  stop() {
    this.done = true;
    return this.value;
  }

  abrupt(type: "return" | "throw", args) {
    if (type === "return") {
      this.value = args;
      this.method = "return";
      this.next = "end";
    } else if (type === "throw") {
      throwargs; }}}Copy the code

Five, the yield * genFn ()

Finally, although you may not use the Generator enough, it is incomplete without it

  1. The functionality left out of “await” and “async” is that one Generator listens to the execution of another Generator. In fact, with await we do not know how many await the child function has experienced
async function a() {
  const res = await b();
}

async function b() {
  await 1;
  await 'str';
  return { data: 'lawler'.msg: 'ok' };
}
Copy the code
  1. So how does this function work at the yield level? In effect, yield* replaces Foo with the delegateYield method, looping through the context so that the yield completes in a case
function gen$(_context) {
  switch (_context.next) {
    case 0:
      _context.next = 7;
      return wait(10);
    case 7:
      // Pass foo Generator Object to gen's context
      return _context.delegateYield(foo(), "t2".8);
    case "end":
      return_context.stop(); }}Copy the code
  1. Inside the wrap, loop execution
generator._invoke = function invoke(method, args) {
  context.method = method;

  // yield* genFn, returns the result of the genFn iteration until return
  while (true) {
    const delegate = context.delegate;
    if (delegate) {
      const delegateResult = maybeInvokeDelegate(delegate, context);
      if (delegateResult) {
        if (delegateResult === empty) continue;
        {value, done}
        returndelegateResult; }}}if (method === "next") {}}Copy the code

The last

  1. This article simply implements Generator, but regenerator does a lot more than throw error, yield* gen() handling, and other handy apis. Dive in if you like

  2. Through the explanation of the working principle of Generator in this article, let us have a deeper understanding of the concept of “coroutine”, which has the effect of “cutting the knife without error” for the things we need to use every day and the debugging code

  3. Source code: regenerator

  4. Code word is not easy, like small partners, remember to leave your small ❤️ oh ~

The resources

  • MDN Generator

  • MDN Iteration protocols

  • regenerator

  • co