当前位置:嗨网首页>书籍在线阅读

14-生成器

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

14.4 生成器

已经在第12章中讨论过,生成器允许函数和其调用方之间的双向通信。它是一个天生的同步,但如果与promise结合起来,它们就能为管理JavaScript中的异步代码提供强大的技术支持。

来重新看看异步代码中最主要的难点:异步代码比同步代码更难编写。当试图解决一个问题时,通常总是希望以同步的方式来解决它:第一步怎么做,然后是第二步,第三步,以此类推。但是这样解决问题可能会产生性能问题,这就是为什么会有异步。如果能够既享受异步代码带来的性能优化,又避免额外接触那些复杂的概念,岂不是两全其美。这也正是生成器的作用。

回想一下之前在“回调地狱”中讲到的例子:读取三个文件的内容,等待一分钟后,将从前三个文件中读到的内容写入第四个文件。以正常的思维大家可能会写出伪代码:

dataA = read contents of 'a.txt'
dataB = read contents of 'b.txt'
dataC = read contents of 'c.txt'
wait 60 seconds
write dataA + dataB + dataC to 'd.txt'

生成器可以让大家写出跟上面这些代码非常类似的代码,但是却不会直接实现想要的功能,需要做一些事情才行。要做的第一件事就是找到一个能将Node中错误优先的回调转化成promise的办法。会把它封装成一个叫作nfcall的函数(Node函数调用):

function nfcall(f, ...args) {
   return new Promise(function(resolve, reject) {
      f.call(null, ...args, function(err, ...args) {
         if(err) return reject(err);
         resolve(args.length<2 ? args[0] : args);
      }); 
   }); 
} 
这个函数是以Q promise库中的 `nfcall` 函数命名的。如果需要这个功能,那么应该使用Q这个库。它不仅包含了这个方法,同时还有很多有用的跟promise相关的方法。在这里展示 `nfcall` 的实现,是为了说明其实这并不难。

接下来可以将任何Node格式的方法转化成接收一个回调的promise。同时也需要 setTimeout ,它可以接收一个回调……但由于它的出现早于Node,因而并不适用于错误优先的惯例。所以这里会创建 ptimeout (promise超时):

function ptimeout(delay) {
   return new Promise(function(resolve, reject) {
      setTimeout(resolve, delay);
   });
}

下一步就是需要一个生成器运行器。回想一下生成器并不是天生异步的。但是由于生成器允许函数和其调用方对话,所以可以创建一个用来管理对话的函数,同时这个函数需要知道如何处理异步调用。这里会创建一个叫作 grun (generator run)的函数:

function grun(g) {
   const it = g();
   (function iterate(val) {
      const x = it.next(val);
      if(!x.done) {
         if(x.value instanceof Promise) {
            x.value.then(iterate).catch(err => it.throw(err));
         } else {
            setTimeout(iterate, 0, x.value);
         } 
      } 
  })();
} 
`grun` 严重依赖于 `runGenerator` ,它在Kyle Simpson写的一系列关于生成器的优秀文章中出现过。强烈建议大家读一下这些文章,来作为内容的补充。

这是一个非常现代化的递归的生成器运行器。当传入一个生成器函数时,它会返回这个函数。就像在第6章学到的,生成器会在调用 yield 的时候暂停运行,直到在它们的迭代器上调用next函数。这是一个递归函数。如果迭代器返回一个promise,它会在promise被满足后才继续运行。另一方面,如果迭代器返回一个简单值,那么函数就会立即继续执行迭代部分。大家可能会奇怪为什么调用 setTimeout ,而不是直接调用 iterate 。原因就在于,通过避免同步的递归调用我们能够获得一些性能上的优化(异步递归允许JavaScript更快速地将可用资源投入使用)。

读者可能会觉得“这太小题大做了!”或者“这可以让编程更简单吗?”,不要担心,复杂的部分到这里就结束了。 nfcall 允许将过去的方式(Node中的错误优先回调函数)用在现在(promise)的代码中,而 grun 允许使用未来(在ES7会出现一个叫作 await 的关键字,其作用与函数 grun 一样,同时语法也更自然)的技术。终于讲解完复杂的部分了,现在来看看这些东西如何让开发人员更轻松地写代码。

还记得本章前面出现的“可以变得更好吗”的伪代码吗?现在大家可以编写实现了:

function* theFutureIsNow() {
   const dataA = yield nfcall(fs.readFile, 'a.txt');
   const dataB = yield nfcall(fs.readFile, 'b.txt');
   const dataC = yield nfcall(fs.readFile, 'c.txt');
   yield ptimeout(60*1000);
   yield nfcall(fs.writeFile, 'd.txt', dataA+dataB+dataC);
}

这看起来比回调地狱好很多,不是吗?同时也比只使用promise要整洁一些。它改变了原有的思维方式。运行起来也很简单:

grun(theFutureIsNow);