Generator函数分析


Generator 分析

理解 Generator

前面已经介绍过 Generator 是 ES6 提出的新语法用于解决传统的”回调地狱”和 Promise 执行过程无法暂停的问题。那我们现在看一看 Generator 的简单用法:

function* helloWorldGenerator() {
  yield "value 1";
  yield "value 2";
  yield "value 3";
}
const gen = helloWorldGenerator();
console.log(gen.next().value); // value 1
console.log(gen.next().value); //value 2
console.log(gen.next().value); //value 3

但是 Generator 内部是如何实行的呢?我们来在babel 官网ES5 环境下是如何实现的:

"use strict";

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

function helloWorldGenerator() {
  return regeneratorRuntime.wrap(function helloWorldGenerator$(_context) {
    while (1) {
      switch ((_context.prev = _context.next)) {
        case 0:
          _context.next = 2;
          return "value 1";

        case 2:
          _context.next = 4;
          return "value 2";

        case 4:
          _context.next = 6;
          return "value 3";

        case 6:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}
var gen = helloWorldGenerator();
console.log(gen.next().value); // value 1
console.log(gen.next().value); //value 2
console.log(gen.next().value); //value 3

上述代码并不长,看起来是使用了链表数据结构,通过判断_context.next 的值来进行返回,但是其中有两个方法我们似乎没有见过:regeneratorRuntime.mark 和 regeneratorRuntime.wrap,经过查询得知,在使用regenerator编译 Generator 获得完成版代码runtime.js之后可以看出,上面两个不认识的方法其实是 regenerator-runtime 模块里的两个方法,完整版代码有 700 多行,我们主要看一下 regeneratorRuntime.mark 和 regeneratorRuntime.wrap 相关部分。

regeneratorRuntime.mark

我们先看一下runtime.js中对于 regeneratorRuntime.mark 的定义:

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

这里有一些定义比如:GeneratorFunctionPrototype、toStringTagSymbol、Gp 等定义我们不知道是干啥的,但是这段代码总的目的还是比较清楚,就是给生成器函数 genFun 绑定了一系列的原型。

regeneratorRuntime.wrap

由之前 babel 转化的代码可以看出执行 helloWorldGenerator() 实际上就是在执行 wrap(),我们来开一下 wrap()方法的定义:

// outerFn.prototype其实就是genFun.prototype
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;
}
exports.wrap = wrap;

从上面代码可以看出 wrap 方法创建了一个 generator,并继承了 protoGenerator.prototype 也就是 outerFn.prototype;然后 new 了一个 Context 对象;后面将 makeInvokeMethod 挂到 generator._invoke 上,并且能接受 innerFn, this, context,最后 return generator。其实 wrap 也就是给 generator 加了一个_invoke 方法。

其中 Context 对象可以理解为一个全局对象其中存储了各种状态和上下文。而 makeInvokeMethod 方法定义如下,它 return 了一个 invoke 方法,通过判断当前的状态 state 来决定下一步操作,其实就是我们调用的 next()方法。

function makeInvokeMethod(innerFn, self, context) {
  var state = GenStateSuspendedStart;

  return function invoke(method, arg) {
    if (state === GenStateExecuting) {
      throw new Error("Generator is already running");
    }

    if (state === GenStateCompleted) {
      if (method === "throw") {
        throw arg;
      }

      // Be forgiving, per 25.3.3.3.3 of the spec:
      // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume
      return doneResult();
    }

    context.method = method;
    context.arg = arg;

    while (true) {
      var delegate = context.delegate;
      if (delegate) {
        var delegateResult = maybeInvokeDelegate(delegate, context);
        if (delegateResult) {
          if (delegateResult === ContinueSentinel) continue;
          return delegateResult;
        }
      }

      if (context.method === "next") {
        // Setting context._sent for legacy support of Babel's
        // function.sent implementation.
        context.sent = context._sent = context.arg;
      } else if (context.method === "throw") {
        if (state === GenStateSuspendedStart) {
          state = GenStateCompleted;
          throw context.arg;
        }

        context.dispatchException(context.arg);
      } else if (context.method === "return") {
        context.abrupt("return", context.arg);
      }

      state = GenStateExecuting;

      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,
        };
      } else if (record.type === "throw") {
        state = GenStateCompleted;
        // Dispatch the exception by looping back around to the
        // context.dispatchException(context.arg) call above.
        context.method = "throw";
        context.arg = record.arg;
      }
    }
  };
}

所以现在再回头看看文章开头的源码,我们会发现,生成器函数就是根据 yield 语句将代码分割为 switch-case 块,后续通过切换_context.prev 和_context.next 来分别执行各个 case。我们来实现一个简陋版的 Generator 函数:

function gen$(_context) {
  while (1) {
    switch ((_context.prev = _context.next)) {
      case 0:
        _context.next = 2;
        return "value 1";
      case 2:
        _context.next = 4;
        return "value 2";
      case 4:
        _context.next = 6;
        return "value 3";
      case 6:
      case "end":
        return _context.stop();
    }
  }
}
// 简陋版context
let context = {
  next: 0,
  prev: 0,
  done: false, // 记录迭代器是否执行完毕
  sent: null, // 记录next()传入的值
  stop: function stop() {
    this.done = true;
  },
};
// 简陋版invoke也就是next
let gen = function () {
  return {
    next: function (val) {
      context.sent = val;
      value = context.done ? undefined : gen$(context);
      done = context.done;
      return { value, done };
    },
  };
};

let g = gen();
console.log(g.next()); // { value: 'value 1', done: false }
console.log(g.next()); // { value: 'value 2', done: false }
console.log(g.next()); // { value: 'value 3', done: false }
console.log(g.next()); // { value: undefined, done: true }

所以整个函数的调用流程就是:

  1. 用*定义 Generator 函数,并被转化成 ES5 的代码;
  2. 转化后的代码主要分三大块:
  • gen$(_context)由 yield 分割代码而来
  • context 对象用于全局储存函数执行的上下文
  • invoke 方法返回 next 方法,用于执行 gen$(_context)和返回值
  1. 在调用 g.next()的时候实际调用的是 invoke()方法,执行 gen$(_context)时,实际上是根据 context.next 的值判断不同的情况返回不同的值;
  2. 当 Generator 函数运行到末尾时,就调用_context 的 stop()方法将 cone 设置为 true 表示生成器所有状态已经遍历完,此时 g.next()返回的是{ value: undefined, done: true }。

由上所述,Generator 的核心部分在于上下文的保存,函数本身并没有被挂起,每一次 yield 其实都是执行了一遍传入的生成器的函数,只不过因为 context 记录了上一次的执行结果,所以直接可以从上一次执行结果开始执行,看起来就想函数被挂起了一样。

参考


文章作者: CassielLee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 CassielLee !
评论
  目录