ES6 Generator 原理

作者: 商领云 | 来源:发表于2016-09-01 09:48 被阅读415次

    文章摘要

    Generator 是 ES6 添加的一个特性,允许函数的暂停和恢复,本文使用 generator 构建了一个惰性队列,并分析其原理。

    文章正文

    编写正确运行的软件可能是困难的,但是我们知道这仅仅是挑战的开始。对一个好的解决方案建模可以把编程从 “可以运行” 转换到 “这样做更好”。相对的,有些事就像下面的注释一样:

    //
    //亲爱的代码维护者:
    // 
    //一旦你试图优化代码,
    //并且最终意识到这是一个错误的决定,
    //请把下面的变量加一,
    //来警告下一个家伙
    //
    //total_hours_wasted_here = 42
    //
    

    (源自: http://stackoverflow.com/questions/184618/what-is-the-best-comment-in-source-code-you-have-ever-encountered/482129#482129)

    今天,我们会去探索 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.markregeneratorRuntime.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

    相关文章

      网友评论

        本文标题:ES6 Generator 原理

        本文链接:https://www.haomeiwen.com/subject/onnqettx.html