Generator 是 ES6 添加的一个特性,容许函数的暂停和恢复,本文使用 generator 构建了一个惰性队列,并分析其原理。javascript
编写正确运行的软件多是困难的,可是咱们知道这仅仅是挑战的开始。对一个好的解决方案建模能够把编程从 “能够运行” 转换到 “这样作更好”。相对的,有些事就像下面的注释同样:前端
// //亲爱的代码维护者: // //一旦你试图优化代码, //而且最终意识到这是一个错误的决定, //请把下面的变量加一, //来警告下一个家伙 // //total_hours_wasted_here = 42 //
(源自: http://stackoverflow.com/ques...java
今天,咱们会去探索 ES6 Generators 的工做原理,从而更好的理解它并用新的方式来解决一些老的问题。node
你可能已经听过编写非阻塞 javascript 代码的重要性。当咱们处理 I/O 操做,好比发送 HTTP 请求或者写数据库,咱们一般都会使用回调或者 promises。阻塞代码会冻结整个应用,在绝大多数场景下都不是一个能够被使用的方案。webpack
这样作的另一个后果是,若是你写了一段无限循环的 javascript 代码,好比git
node -e 'while(true) {}'
它将极可能冻结你的电脑而且须要系统重启,请不要在家尝试。github
考虑到这些,当听到 ES6 Generators 容许咱们在函数的中间暂停执行而后在将来的某个时候恢复执行时,感到很是好奇。
虽然有些工具好比 Regenerator 和 Babel 已经把这些特性部署到了 ES5。你是否困惑过它们是怎样作到这些的?今天,咱们会找到真相。
但愿咱们能够更深刻的理解 generators, 更好的发挥它的做用。web
让咱们从一个简单的例子开始。好比你要操做一个序列,你可能会建立一个数组而且按照数组的方式操做其值。可是若是这个序列是无限长的呢?数组就不行了,咱们可使用 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 时返回结果。npm
咱们能够读出上面函数的意图 “每次你请求下一个值,它都会给你一个从 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 }
为了更好的理解 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
翻译自MaxLeap团队_前端研发人员:Henry Bai
译者简介:多年后端及前端开发经验,现任MaxLeap UX团队成员,主要从事MaxLeap相关开发,目前对React Native有浓厚兴趣。
中文连接:https://blog.maxleap.cn/archi...
相关文章
webpack 入门
做者往期佳做
Redux 最佳实践「译」
欢迎订阅微信公众号:MaxLeap_yidongyanfa
关于MaxLeap
MaxLeap 移动业务研发的云服务平台,为企业提供包括应用开发所需的后端云数据库、云数据源、云代码、云容器、 IM、移动支付、应用内社交、第三方登陆、社交分享、数据分析、推送营销,用户支持等服务, MaxLeap 致力于让移动应用开发更快速简单。
官网:https://maxleap.cn/