在《深入浅出Node.js》的第4章里,笔者深度地介绍了当前盛行在Node和前端JavaScript中的几种异步编程的解决方案,唯独对Generator的解决方案没有介绍。但随着Node版本的升级和ECMAScript harmony的特性不断得到支持,0.11中,通过启用--harmory
参数,可以让V8支持Generator。最近Connect/Express背后的开发团队也将精力转移到新的库和框架上,这个核心库和框架就是co
和koa
,它们最主要的特点就是主要基于ECMAScript harmony中的Generator特性,这使得它在异步编程方面有较优雅的实现。
本文将深度介绍下Generator是如何实现将异步编程从原始的嵌套式代码转换成扁平的顺序式代码。
简单地回顾下,异步编程的问题主要有必须通过回调函数进行返回值的处理,以及复杂情况下会造成嵌套过深的问题。这里简单地给出两种典型的异步场景。
异步串行读取文件:
fs.readFile('file1.txt', 'utf8', function (err, txt) {
if (err) {
throw err;
}
fs.readFile(txt, 'utf8', function (err, content) {
if (err) {
throw err;
}
console.log(content);
});
});
异步并行读取文件:
fs.readFile('file1.txt', 'utf8', function (err, txt) {
if (err) {
throw err;
}
console.log(txt);
});
fs.readFile('file2.txt', 'utf8', function (err, content) {
if (err) {
throw err;
}
console.log(content);
});
上述两种场景下,可以看到串行时由于代码嵌套,当调用更多时,无疑代码会更糟糕。对于异步并行读取文件的代码,难点是无法获知并行异步调用完全完成的时间点,要解决这个问题需要借助各种异步流程库。
目前主流地解决异步流程控制问题的方案主要有以下三种:
- 自定义事件式方案。
- Promise/Deferred。
- 高阶函数篡改回调函数。
由于在《异步编程》章节中已经充分介绍了上述三种方式的实现形式,这里不再详细展开三种方式的细节。但是为了行文承前启后,这里简单回顾下第三种方案的实现。
高阶函数在异步编程中的使用,最广泛和最知名莫过于async
和step
两个库,它将用户正常传递进来的回调函数替换成自己包装了特殊逻辑的函数,然后再传递给异步调用。当异步调用结束后,先执行的是特殊逻辑,然后才是用户传入的回调函数。以一个简单的场景为例,假设需要等待所有异步回调执行完成后,才能执行某个逻辑。这时通过高阶函数篡改回调函数的方式就大为受用,也相当简单。以下为简单实现:
var pending = (function () {
var count = 0;
return function (callback) {
count++;
return function () {
count--;
if (count === 0) {
callback();
}
};
};
}());
var done = pending(function () {
console.log('all is over');
});
fs.readFile('file1.txt', 'utf8', done());
fs.readFile('file2.txt', 'utf8', done());
上述代码中,done
执行了两次,每次执行的过程中,将计数器count
加一,然后返回一个函数。当fs.readFile
这个异步调用结束后,done
执行后的回调函数会得到执行,计数器减一。当计数器回到0的时候,意味着多个异步调用的回调函数都已经执行,此时执行传入的回调函数。因为非阻塞的原因,done()
生成的函数不会立即执行,使得计数器可以正常地增加值,结束后才慢慢减少值。
抛开异步调用不谈,高阶函数的试用上,要让用户传入的函数能得到执行。需要如下这种方式的调用:
var done = pending(function () {
console.log('all is over');
});
done()();
这里生成的函数被立即调用了,count
这个计数器加一后,立即减一,然后触发等于零的条件,于是回调函数被执行了。
在本文中,高阶函数的使用与Generator之间会产生相当微妙的化学反应。