regenerator的效果

regenerator是一个es5环境下实现es6的generator的方式,

regenerator 分为编译时和运行时两块,编译工具会负责将generator编译成es5,比如:

这段代码:

const len = 100;

function *gen() {
  console.log('gen !');
  for (let i = 0; i < len; i++) {
    yield i;
  }
  return 'what';
}

const g = gen();
for (let i = 0; i < len; i++) {
  console.log(g.next());
}

会被编译成

'use strict';

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

var len = 10;

function gen() {
  var i;
  return regeneratorRuntime.wrap(function gen$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          console.log('gen !');
          i = 0;

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

          _context.next = 5;
          return i++;

        case 5:
          _context.next = 2;
          break;

        case 7:
          return _context.abrupt("return", 'what');

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

var g = gen();

for (var i = 0; i < len; i++) {
  console.log(g.next()); 
}

其中的regeneratorRuntime就是regenerator的运行时部分。

编译后产生的 switch 代码可以理解为定义的各个状态,以及状态间转化的逻辑

运行时

运行时我们主要看一下regeneratorRuntime这个对象提供的内容。regeneratorRuntime的源代码地址

regeneratorRumtime在上面编译后的代码里,提供了两个方法,wrapmark

wrap

wrap方法会返回一个generator对象,也就是符合es6标准的迭代器,包含方法:

  • next
  • throw
  • return

按照迭代器的定义,调用next方法会是迭代器从上次yield的位置后面继续执行。每调用一次next方法,都会调用一次编译后的gen$ 方法。$gen 方法内会修改context上下文中的next值,达到一个驱动迭代器的效果。

gen$ 这个函数很像是一个reducer
// line 274
if (context.method === "next") {
          // Setting context._sent for legacy support of Babel's
          // function.sent implementation.
          context.sent = context._sent = context.arg;
} //...

// line 293
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
  };

} // ...

这里的innerFn就是之前的 gen$ 函数,tryCatch(innerFn, self, context) 就会调用 gen$ 函数,gen$ 会返回一个结果,如果没有异常,就会返回next 方法的标准返回 { value: any, done: boolean }

tryCatch 方法 会捕获 gen$ 中抛出的异常。如果有捕获到,则会返回异常的状态

// line 61
function tryCatch(fn, obj, arg) {
  try {
    return { type: "normal", arg: fn.call(obj, arg) };
  } catch (err) {
    return { type: "throw", arg: err };
  }
}

mark

mark方法会将gen 迭代器函数的原型指向runtime内部定义的generator对象

那么regenerator内部是怎么定义generator对象的呢?

// line 82
  function Generator() {}
  function GeneratorFunction() {}
  function GeneratorFunctionPrototype() {}

  // This is a polyfill for %IteratorPrototype% for environments that
  // don't natively support it.
  var IteratorPrototype = {};
  IteratorPrototype[iteratorSymbol] = function () {
    return this;
  };

  var getProto = Object.getPrototypeOf;
  var NativeIteratorPrototype = getProto && getProto(getProto(values([])));
  if (NativeIteratorPrototype &&
      NativeIteratorPrototype !== Op &&
      hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) {
    // This environment has a native %IteratorPrototype%; use it instead
    // of the polyfill.
    IteratorPrototype = NativeIteratorPrototype;
  }

  var Gp = GeneratorFunctionPrototype.prototype =
    Generator.prototype = Object.create(IteratorPrototype);
  GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;
  GeneratorFunctionPrototype.constructor = GeneratorFunction;
  GeneratorFunction.displayName = define(
    GeneratorFunctionPrototype,
    toStringTagSymbol,
    "GeneratorFunction"
  );
// line 111

// line 115
 function defineIteratorMethods(prototype) {
    ["next", "throw", "return"].forEach(function(method) {
      define(prototype, method, function(arg) {
        return this._invoke(method, arg);
      });
    });
  }

// line 406
  defineIteratorMethods(Gp);

line 82 - line 111 定义了几个dummy constructor,并且定义了它们之间原型链关系,这些关系的设置是按照 ES6 规范 来设置的。

line 115 defineIteratorMethods 声明了一个定义generator内部方法的函数,函数中 define 方法内部实际是 Object.define 方法的封装。

line 406 使用 defineIteratorMethods,传入Gp,使得Gp成为一个generator对象。defineIteratorMethods定义了next, throw, return 三个方法

context

generator的上下文,即状态机的上下文,提供了多个方法和变量,《runtime.js#L521

方法:

  • reset
  • stop
  • dispatchException
  • abrupt
  • complete
  • finish
  • catch

变量:这些变量的初始化都在reset方法里,reset方法会在构造函数里面被调用

  • next number - 状态机的下一个状态值
  • pre number - 状态机的上一个状态值
  • sent
  • done
  • delegate
  • method
  • arg

编译时

编译时,迭代器里的内容会被编译成switch...case形式的代码,其中各个case基本是以yield关键词作为划分case的依据,然后整个switch...case会被包裹在一个while(1)的循环里,只有当某一个case使用return返回时才会跳出while循环。

比如一个简单的,可迭代2次的迭代器

'编译前'
function *gen() {
  yield 1;
  yield 2;
}

其中的2次yield,在编译结果里,就会被拆成2个case, case 0 和 case 2

'编译后'
function gen() {
  return regeneratorRuntime.wrap(function gen$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 1;

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

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

那如果迭代器里面有for循环呢,是怎么处理的?

'编译前'
function *gen() {
  for (let i = 0; i < len; i++) {
     before = 'yield前语句'
     yield i;
     after = 'yield后语句'
  }
}

for循环中,每一次循环会被拆成多个case

function gen() {
  var i;
  return regeneratorRuntime.wrap(function gen$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          i = 0;

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

          before = 'yield前语句';
          _context.next = 5;
          return i;

        case 5:
          after = 'yield后语句';

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

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


for 循环会被拆成几部分,首先for(...)括号里的三部分会被拆成三个部分

  • initialization - 初始化语句 对应 case 0
  • condition - 条件 对应 case 1 中开头部分
  • final expression - 最终表达式 对应 case 6

for循环内部逻辑:

  • case 1 和 case 5 分别表示for的大括号里面,yield前和yield后代码。

参考:

ES6 系列之 Babel 将 Generator 编译成了什么样子

从协程到状态机--regenerator源码解析(一)


TARS
1 声望0 粉丝