返回介绍

第一部分 类型和语法

第二部分 异步和性能

4.8 ES6 之前的生成器

发布于 2023-05-24 16:38:21 字数 6325 浏览 0 评论 0 收藏 0

现在,希望你已经相信,生成器是异步编程工具箱中新增的一种非常重要的工具。但是,这是 ES6 中新增的语法,这意味着你没法像对待 Promise(这只是一种新的 API)那样使用生成器。所以如果不能忽略 ES6 前的浏览器的话,怎么才能把生成器引入到我们的浏览器 JavaScript 中呢?

对 ES6 中所有的语法扩展来说,都有工具(最常见的术语是 transpiler,指 trans-compiler,翻译编译器)用于接收 ES6 语法并将其翻译为等价(但是显然要丑陋一些!)的前 ES6 代码。因此,生成器可以被翻译为具有同样功能但可以工作于 ES5 及之前的代码。

可怎么实现呢?显然 yield 的“魔法”看起来并不那么容易翻译。实际上,我们之前在讨论基于闭包的迭代器时已经暗示了一种解决方案。

4.8.1 手工变换

在讨论 transpiler 之前,先来推导一下对生成器来说手工变换是如何实现的。这不只是一个理论上的练习,因为这个练习实际上可以帮助我们更深入理解其工作原理。

考虑:

// request(..)是一个支持Promise的Ajax工具

function *foo(url) {
  try {
    console.log( "requesting:", url );
    var val = yield request( url );
    console.log( val );
  }
  catch (err) {
    console.log( "Oops:", err );
    return false;
  }
}

var it = foo( "http://some.url.1" );

首先要观察到的是,我们仍然需要一个可以调用的普通函数 foo() ,它仍然需要返回一个迭代器。因此,先把非生成器变换的轮廓刻画出来:

function foo(url) {

  // ..

  // 构造并返回一个迭代器
  return {
    next: function(v) {
      // ..
    },
    throw: function(e) {
      // ..
    }
  };
}

var it = foo( "http://some.url.1" );

接下来要观察到的是,生成器是通过暂停自己的作用域 / 状态实现它的“魔法”的。可以通过函数闭包(参见本系列的《你不知道的 JavaScript(上卷)》的“作用域和闭包”部分)来模拟这一点。为了理解这样的代码是如何编写的,我们先给生成器的各个部分标注上状态值:

// request(..)是一个支持Promise的Ajax工具

function *foo(url) {
  // 状态1

  try {
    console.log( "requesting:", url );
    var TMP1 = request( url );

    // 状态2
    var val = yield TMP1;
    console.log( val );
  }
  catch (err) {
    // 状态3
    console.log( "Oops:", err );
    return false;
  }
}

为了更精确地展示,我们使用临时变量 TMP1 把 val = yield request.. 语句分成了两个部分。request(..) 在状态 1 发生,其完成值赋给 val 发生在状态 2 。当我们把代码转换成其非生成器等价时,会去掉这个中间变量 TMP1 。

换句话说,1 是起始状态,2 是 request(..) 成功后的状态,3 是 request(..) 失败的状态。你大概能够想象出如何把任何额外的 yield 步骤编码为更多的状态。

回到我们翻译的生成器,让我们在闭包中定义一个变量 state 用于跟踪状态:

function foo(url) {
  // 管理生成器状态
  var state;

  // ..
}

现在在闭包内定义一个内层函数,称为 process(..) ,使用 switch 语句处理每个状态:

// request(..)是一个支持Promise的Ajax工具

function foo(url) {
  // 管理生成器状态
  var state;

  // 生成器范围变量声明
  var val;

  function process(v) {
    switch (state) {
      case 1:
        console.log( "requesting:", url );
        return request( url );
      case 2:
        val = v;
        console.log( val );
        return;
      case 3:
        var err = v;
        console.log( "Oops:", err );
        return false;
    }
  }

  // ..
}

我们生成器的每个状态都在 switch 语句中由自己的 case 表示。每次需要处理一个新状态的时候就会调用 process(..) 。稍后我们将会回来介绍这是如何工作的。

对于每个生成器级的变量声明(val ),我们都把它移动为 process(..) 外的一个 val 声明,这样它们就可以在多个 process(..) 调用之间存活。不过块作用域的变量 err 只在状态 3 中需要使用,所以把它留在原来的位置。

在状态 1 ,没有了 yield resolve(..) ,我们所做的是 return resolve(..) 。在终止状态 2 ,没有显式的 return ,所以我们只做一个 return ,这等价于 return undefined 。在终止状态 3 ,有一个 return false ,因此就保留这一句。

现在需要定义迭代器函数的代码,使这些函数正确调用 process(..) :

function foo(url) {
  // 管理生成器状态
  var state;

  // 生成器变量范围声明
  var val;

function process(v) {
  switch (state) {
    case 1:
      console.log( "requesting:", url );
      return request( url );
    case 2:
      val = v;
      console.log( val );
      return;
    case 3:
      var err = v;
      console.log( "Oops:", err );
      return false;
  }
}

// 构造并返回一个生成器
return {
  next: function(v) {
    // 初始状态
    if (!state) {
      state = 1;
      return {
        done: false,
        value: process()
      };
    }
    // yield成功恢复
    else if (state == 1) {
      state = 2;
      return {
        done: true,
        value: process( v )
      };
    }
    // 生成器已经完成
    else {
      return {
        done: true,
        value: undefined
      };
    }
  },
  "throw": function(e) {
    // 唯一的显式错误处理在状态1
    if (state == 1) {
      state = 3;
      return {
        done: true,
        value: process( e )
        };
      }
      // 否则错误就不会处理,所以只把它抛回
      else {
        throw e;
      }
    }
  };
}

这段代码是如何工作的呢?

(1) 对迭代器的 next() 的第一个调用会把生成器从未初始化状态转移到状态 1 ,然后调用 process() 来处理这个状态。request(..) 的返回值是对应 Ajax 响应的 promise,作为 value 属性从 next() 调用返回。

(2) 如果 Ajax 请求成功,第二个 next(..) 调用应该发送 Ajax 响应值进来,这会把状态转移到状态 2 。再次调用 process(..) (这次包括传入的 Ajax 响应值),从 next(..) 返回的 value 属性将是 undefined 。

(3) 然而,如果 Ajax 请求失败的话,就会使用错误调用 throw(..) ,这会把状态从 1 转移到 3 (而非 2 )。再次调用 process(..) ,这一次包含错误值。这个 case 返回 false ,被作为 throw(..) 调用返回的 value 属性。

从外部来看(也就是说,只与迭代器交互),这个普通函数 foo(..) 与生成器 *foo(..) 的工作几乎完全一样。所以我们已经成功地把 ES6 生成器转为了前 ES6 兼容代码!

然后就可以手工实例化生成器并控制它的迭代器 了,调用 var it = foo("..") 和 it.next(..) 等。甚至更好的 是,我们可以把它传给前面定义的工具 run(..) ,就像 run(foo,"..") 。

4.8.2 自动转换

前面的 ES6 生成器到前 ES6 等价代码的手工推导练习,向我们教授了概念上生成器是如何工作的。但是,这个变换非常复杂,并且对于代码中的其他生成器而言也是不可移植的。这部分工作通过手工实现十分不实际,会完全抵消生成器的一切优势。

但幸运的是,已经有一些工具可以自动把 ES6 生成器转化为前面小节中我们推导出来的结果那样的代码。它们不仅会为我们完成这些笨重的工作,还会处理我们忽略的几个枝节问题。

regenerator 就是这样的一个工具(http://facebook.github.io/regenerator/ ),出自 Facebook 的 几个聪明人。

如果使用 regenerator 来转换前面的生成器的话,以下是产生的代码(本书写作之时):

// request(..)是一个支持Promise的Ajax工具

var foo = regeneratorRuntime.mark(function foo(url) {
  var val;

  return regeneratorRuntime.wrap(function foo$(context$1$0) {
    while (1) switch (context$1$0.prev = context$1$0.next) {
    case 0:
      context$1$0.prev = 0;
      console.log( "requesting:", url );
      context$1$0.next = 4;
      return request( url );
    case 4:
      val = context$1$0.sent;
      console.log( val );
      context$1$0.next = 12;
      break;
    case 8:
      context$1$0.prev = 8;
      context$1$0.t0 = context$1$0.catch(0);
      console.log("Oops:", context$1$0.t0);
      return context$1$0.abrupt("return", false);
    case 12:
    case "end":
      return context$1$0.stop();
    }
  }, foo, this, [[0, 8]]);
});

这与我们手工推导的结果有一些明显的相似之处,比如那些 switch/case 语句,而且我们甚至看到了移出闭包的 val ,就像我们做的一样。

当然,一个不同之处是,regenerator 的变换需要一个辅助库 regeneratorRuntime ,其中包含了管理通用生成器和迭代器的所有可复用逻辑。这些重复代码中有很多和我们的版本不同,但即使这样,很多概念还是可以看到的,比如 context$1$0.next = 4 记录生成器的下一个状态。

主要的收获是,生成器不再局限于只能在 ES6+ 环境中使用。一旦理解了这些概念,就可以在代码中使用,然后使用工具将其变换为与旧环境兼容的代码。

这比仅仅将修改后的 Promise API 用作前 ES6 Promise 所做的工作要多得多,但是,付出的代价是值得的,因为在实现以合理的、明智的、看似同步的、顺序的方式表达异步流程方面,生成器的优势太多了。

一旦迷上了生成器,就再也不会想回到那一团乱麻的异步回调地狱中了。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文