「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」
说在前面话
编写正确运行的软件可能是困难的,但是我们知道这仅仅是挑战的开始。对一个好的解决方案建模可以把编程从 “可以运行” 转换到 “这样做更好”。相对的,有些事就像下面的注释一样:
//
//亲爱的代码维护者:
//
//一旦你试图优化代码,
//并且最终意识到这是一个错误的决定,
//请把下面的变量加一,
//来警告下一个家伙
//
//total_hours_wasted_here = 42
//
今天,我们会去探索 ES6 Generators 的工作原理,从而更好的理解它并用新的方式来解决一些老的问题。
异步/非阻塞
你可能已经听过编写非阻塞 javascript 代码的重要性。当我们处理 I/O 操作,比如发送 HTTP 请求或者写数据库,我们通常都会使用回调或者 promises。阻塞代码会冻结整个应用,还会浪费cpu资源,在绝大多数场景下都不是一个可以被使用的方案。
这样做的另外一个后果是,如果你写了一段无限循环的 javascript 代码,比如
while(true) {}
它很可能卡死你的电脑并且需要系统重启,请不要在自己的电脑上尝试。
ES6 的 Generators 允许我们在函数的中间暂停执行然后在未来的某个时候恢复执行时。
虽然有些工具比如 Regenerator 和 Babel 已经把这些特性实现在了 ES5。但是你有想过过它们是怎样做到这些的?今天,你会找到真相。 希望你可以更深入的理解 generators, 更好的发挥它的作用。
一个惰性序列
让我们从一个简单的例子开始。比如你要操作一个序列,你可能会创建一个数组并且按照数组的方式操作其值。但是如果这个序列是无限长的呢?数组就不行了,我们可以使用 generator 函数来做:
function* generateRandoms (max) {
max = max || 1;
while (true) {
let newMax = yield Math.random() * max;
if (newMax !== undefined) {
max = newMax;
}
}
}
注意 function* 部分,这个 * 表示这是一个 “generator 函数”,并且表现与普通函数不同。另一个重要的部分是 yield 关键字。普通的函数仅仅通过 return 返回结果,而 generator 函数在 yield 时返回结果。
我们可以读出上面函数的意图 “每次你请求下一个值,它都会给你一个从 0 到 max 的值,直到程序退出(直到人类科技毁灭)。
根据上面的代码,我们仅仅在需要时才会得到一个值,这是非常重要的,否则,无限序列会很快的耗尽我们的内存和 CPU。我们使用 迭代器(Iterator) 的 next 方法来获取需要的值:
var iterator = generateRandoms();
console.log(iterator.next()); // { value: 0.4900301224552095, done: false }
console.log(iterator.next()); // { value: 0.8244022422935814, done: false }
Generators 允许两种交互,正如我们下面将要看到的,generators 在没有被调用时会被挂起,而当迭代器请求下一个值时会被唤醒。所以当我们调用 iterator.next 并且传递了参数后,参数会被赋值到 newMax:
console.log(iterator.next()); // { value: 0.4900301224552095, done: false }
// 为 `newMax` 赋值,该值会一直存在
console.log(iterator.next(1000)); // { value: 963.7744706124067, done: false }
console.log(iterator.next()); // { value: 714.516609441489, done: false }
在 ES5 中使用 Generators
为了更好的理解 generators 工作原理,我们可以看一下 generators 是怎样转换成 ES5 代码的。你可以安装 babel 然后看一下它转换后的代码,或者你也可以去 Babel 的官网在线转换
/* eslint-disable */
var _marked = /*#__PURE__*/regeneratorRuntime.mark(generateRandoms);
function generateRandoms(max) {
var newMax;
return regeneratorRuntime.wrap(function generateRandoms$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
max = max || 1;
case 1:
if (!true) {
_context.next = 8;
break;
}
_context.next = 4;
return Math.random() * max;
case 4:
newMax = _context.sent;
if (newMax !== undefined) {
max = newMax;
}
_context.next = 1;
break;
case 8:
case "end":
return _context.stop();
}
}
}, _marked);
}
如你所见,generator 函数的核心代码被转换成了 switch 块, 这对于我们探索其内部原理提供了很有价值的线索。我们可以把 generator 想象成一个循环状态机,它根据我们的交互切换不同的状态。变量 context$1$0 保存了当前的状态,case 语句都根据该状态中执行。
看一下这些 switch 的条件:
case 0:初始化 max 的值并且执行到了case: 1。case 1:返回一个随机值然后GOTO 4case 4:检查迭代器是否设置了 newMax 的值,如果是,就更新 max 的值,然后GOTO 1, 返回一个随机值。
这就解释了为什么 generator 可以在遵循非阻塞的原则上可以无限循环和暂停。
何时退出循环
你可能注意到我跳过了这段代码:
if (!true) {
context$1$0.next = 8;
break;
}
这里发生了什么?它其实是原始代码 while (true) 被转换后的代码(就是对 while 的条件取反,没有其他用意)。
每当状态机循环时,它都会检查是否已经到了最后一步。在我们的示例中,是没有循环结束的,但你在编码时可能会遇到很多时候需要退出循环。当符合循环结束条件时,状态机 GOTO 8, generator 之行完毕。
迭代器中的私有状态
另外一个有趣的事情是 generator 是如何为每一个独立的迭代器保存私有状态的。因为变量 max 在 regeneratorRuntime.wrap 的外层作用域,它的值会被保留以供之后的 iterator.next() 访问。
如果我们调用 randomNumbers() 创建一个新的迭代器,那么一个新的闭包也会被创建。这也就解释了迭代器在使用同一个 generator 时有自己的私有状态而不会相互影响。
状态机内部
目前为止,我们已经看到 switch 的本质就是状态机。你可能已经注意到这个函数被包了两层:regeneratorRuntime.mark,regeneratorRuntime.wrap。
这些是 regenerator 模块,它可以在 ES5 中定义 ES6 generator 形式的状态机。
当 被调用时,会执行 regeneratorRuntime.wrap,传入两个函数,一个是原函数被 babel 编译后的结果(switch),另一个则是 regeneratorRuntime.mark(generateRandoms) 的结果。
function wrap(innerFn, outerFn, self, tryLocsList) {
var generator = Object.create((outerFn || Generator).prototype);
// 获得一个闭包,一个新的状态机开始,
generator._invoke = makeInvokeMethod(
innerFn, self || null,
new Context(tryLocsList || [])
);
return generator;
}
mark做的事情也很简单,就是为函数补全原型对象。使其看起来更像一个 Generator 函数
// 改造 genFun 的原型, 使得 genFun.prototype instanceof Generator 为 true
runtime.mark = function(genFun) {
genFun.__proto__ = GeneratorFunctionPrototype;
genFun.prototype = Object.create(Gp);
return genFun;
};
我们的重点在 wrap 中调用的 makeInvokeMethod 函数。源代码: runtime.js:130-133
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) {
return doneResult();// 会执行 context.next = 0
}
在这里并没有发生什么事,它仅仅是创建并返回了一个函数。这也意味着当我们调用 var iterator = generateRandoms(), generateRandoms 内部并没有执行。
next
当我们调用 iterator.next(), generator 函数(也就是 switch 的那段代码的函数)会在 tryCatch 中被调用:
源代码: runtime.js:234
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;
var info = {
value: record.arg,
done: context.done
};
if (record.arg === ContinueSentinel) {
if (context.delegate && method === "next") {
// Deliberately forget the last sent value so that we don't
// accidentally pass it on to the delegate.
arg = undefined;
}
} else {
return info;
}
} else if (record.type === "throw") {
state = GenStateCompleted;
// Dispatch the exception by looping back around to the
// context.dispatchException(arg) call above.
method = "throw";
arg = record.arg;
}
// tryCatch 如下,只要执行不报错就是 normal
function tryCatch(fn, obj, arg) {
try {
return { type: "normal", arg: fn.call(obj, arg) };
} catch (err) {
return { type: "throw", arg: err };
}
}
如果返回结果是普通的 return (而不是 throw), 会把结果包装成 {value, done}。新的状态是 GenStateCompleted 或者 GenStateSuspendedYield。由于我们的示例是无限循环,所以将总是跳转到 GenStateSuspendedYield 状态。
那么何时会 tryCatch 中何时才会出现 fn 抛出错误呢?通过 fn 我们发现只有 _context.next 为 8 时才会结束,此时会调用 _context.stop()
function generateRandoms$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
max = max || 1;
case 1:
if (!true) {
_context.next = 8;
break;
}
_context.next = 4;
return Math.random() * max;
case 4:
newMax = _context.sent;
if (newMax !== undefined) {
max = newMax;
}
_context.next = 1;
break;
case 8:
case "end":
return _context.stop();
}
}
}
_context.stop 的作用就是抛出错误,源码如下:runtime.js:404-414
stop: function() {
this.done = true;
var rootEntry = this.tryEntries[0];
var rootRecord = rootEntry.completion;
if (rootRecord.type === "throw") {// 抛出错误
throw rootRecord.arg;
}
return this.rval;
},
就这样完成了 generate, Regenerator runtime 是一个很长的话题,这里我们仅仅只结束其中比较核心的部分。
总结一下
今天我们用 generator 函数实现了一个惰性序列状态机。这个特性现在就可以使用: 现代浏览器都已经原生支持了 generator, 而对于老的浏览器,也很容易做代码转换。
其实现原理是先将 generator 函数通过 babel 转化为状态机(switch),然后通过调用 next 方法来不断的执行状态机更新状态 _context.next。
每当执行调用 next 函数就会执行一次 switch 语句,当遇到 return 后就会结束(暂停),然后等待下一次调用 next 继续执行。这就解释了为什么 generator 可以在遵循非阻塞的原则上可以无限循环和暂停
真正厉害的过程是 babel 如何将 generator 转化为状态机的过程,babel yyds。
👍👍👍⭐⭐⭐(暗示)