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);