文章摘要
Generator 是 ES6 添加的一个特性,允许函数的暂停和恢复,本文使用 generator 构建了一个惰性队列,并分析其原理。
文章正文
编写正确运行的软件可能是困难的,但是我们知道这仅仅是挑战的开始。对一个好的解决方案建模可以把编程从 “可以运行” 转换到 “这样做更好”。相对的,有些事就像下面的注释一样:
//
//亲爱的代码维护者:
//
//一旦你试图优化代码,
//并且最终意识到这是一个错误的决定,
//请把下面的变量加一,
//来警告下一个家伙
//
//total_hours_wasted_here = 42
//
今天,我们会去探索 ES6 Generators 的工作原理,从而更好的理解它并用新的方式来解决一些老的问题。
非阻塞
你可能已经听过编写非阻塞 javascript 代码的重要性。当我们处理 I/O 操作,比如发送 HTTP 请求或者写数据库,我们通常都会使用回调或者 promises。阻塞代码会冻结整个应用,在绝大多数场景下都不是一个可以被使用的方案。
这样做的另外一个后果是,如果你写了一段无限循环的 javascript 代码,比如
node -e '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 的值,直到程序退出(直到人类科技毁灭)。”
根据上面的解读,我们仅仅在需要时才会得到一个值,这是非常重要的,否则,无限序列会很快的耗尽我们的内存。我们使用迭代器来获取需要的值:
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 然后看一下它转换后的代码:
npm install -g babel
babel generate-randoms.js
下面是转换后的代码:
var generateRandoms = regeneratorRuntime.mark(function generateRandoms(max) {
var newMax;
return regeneratorRuntime.wrap(function generateRandoms$(context$1$0) {
while (1) switch (context$1$0.prev = context$1$0.next) {
case 0:
max = max || 1;
case 1:
if (!true) {
context$1$0.next = 8;
break;
}
context$1$0.next = 4;
return Math.random() * max;
case 4:
newMax = context$1$0.sent;
if (newMax !== undefined) {
max = newMax;
}
context$1$0.next = 1;
break;
case 8:
case "end":
return context$1$0.stop();
}
}, generateRandoms, this);
});
如你所见,generator 函数的核心代码被转换成了 switch 块, 这对于我们探索其内部原理提供了很有价值的线索。我们可以把 generator 想象成一个循环状态机,它根据我们的交互切换不同的状态。变量 context$1$0
保存了当前的状态,case 语句都在该状态中之行。
看一下这些 switch 块的条件:
-
case 0:
初始化 max 的值并且执行到了case1
。 -
case 1:
返回一个随机值然后GOTO 4
-
case 4:
检查迭代器是否设置了 newMax 的值,如果是,就更新 max 的值,然后GOTO 1
, 返回一个随机值。
这就解释了为什么 generator 可以在遵循非阻塞的原则上可以无限循环和暂停。
何时退出循环
读者可能注意到我跳过了 9-12 行的代码:
if (!true) {
context$1$0.next = 8;
break;
}
这里发生了什么?它其实是原始代码 while (true)
被转换后的代码。每当状态机循环时,它都会检查是否已经到了最后一步。在我们的示例中,是没有循环结束的,但你在编码时可能会遇到很多时候需要退出循环。当符合循环结束条件时,状态机 GOTO 8
, generator 之行完毕。
迭代器中的私有状态
另外一个有趣的事情是 generator 是如何为每一个独立的迭代器保存私有状态的。因为变量 max 在 regeneratorRuntime.wrap
的外层作用域,它的值会被保留以供之后的 iterator.next()
访问。
如果我们调用 randomNumbers()
创建一个新的迭代器,那么一个新的闭包也会被创建。这也就解释了迭代器在使用同一个 generator 时有自己的私有状态而不会相互影响。
状态机内部
目前为止,我们已经看到 switch 的本质就是状态机。你可能已经注意到这个函数被包了两层:regeneratorRuntime.mark
,regeneratorRuntime.wrap
。
这些是 regenerator 模块,它可以在 ES5 中定义 ES6 generator 形式的状态机。
Regenerator runtime 是一个很长的话题,但是我们会覆盖一些有趣的部分。首先,我们可以看到 generator 从 “Suspended Start” 状态开始:
function makeInvokeMethod(innerFn, self, context) {
var state = GenStateSuspendedStart;
return function invoke(method, arg) {
源代码: runtime.js:130,133
在这里并没有发生什么事,它仅仅是创建并返回了一个函数。这也意味着当我们调用 var iterator = generateRandoms()
, generateRandoms 内部并没有执行。
当我们调用 iterator.next()
, generator 函数(之前switch 块中的内容)会在 tryCatch 中被调用:
var record = tryCatch(innerFn, self, context);
源代码: runtime.js:234
如果返回结果是普通的 return (而不是 throw), 会把结果包装成 {value, done}
。新的状态是 GenStateCompleted
或者 GenStateSuspendedYield
。由于我们的示例是无限循环,所以将总是跳转到挂起状态。
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
};
源代码: runtime.js:234,245
你可以用它做什么?
今天我们用 generator 函数实现了一个惰性序列状态机。这个特性现在就可以使用: 现代浏览器都已经原生支持了 generator, 而对于老的浏览器,也很容易做代码转换。
通常,做一件事情的方式有多种。从这种层面上讲,你可能不需要 generators,但如果它允许我们以一种更富表现力的方式完成目标,那就是值得的。
作者信息
原文作者: Josh Johnston
原文链接: http://x-team.com/2015/04/generators-work/
翻译自MaxLeap团队_前端研发人员:Henry Bai
网友评论