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 }
所以整个函数的调用流程就是:
- 用*定义 Generator 函数,并被转化成 ES5 的代码;
- 转化后的代码主要分三大块:
- gen$(_context)由 yield 分割代码而来
- context 对象用于全局储存函数执行的上下文
- invoke 方法返回 next 方法,用于执行 gen$(_context)和返回值
- 在调用 g.next()的时候实际调用的是 invoke()方法,执行 gen$(_context)时,实际上是根据 context.next 的值判断不同的情况返回不同的值;
- 当 Generator 函数运行到末尾时,就调用_context 的 stop()方法将 cone 设置为 true 表示生成器所有状态已经遍历完,此时 g.next()返回的是{ value: undefined, done: true }。
由上所述,Generator 的核心部分在于上下文的保存,函数本身并没有被挂起,每一次 yield 其实都是执行了一遍传入的生成器的函数,只不过因为 context 记录了上一次的执行结果,所以直接可以从上一次执行结果开始执行,看起来就想函数被挂起了一样。