Generators + Promises

将一系列promise在一个链条中表达来代表你程序的异步流程控制是 可能 的。考虑如如下代码:

  1. step1()
  2. .then(
  3. step2,
  4. step1Failed
  5. )
  6. .then(
  7. function step3(msg) {
  8. return Promise.all( [
  9. step3a( msg ),
  10. step3b( msg ),
  11. step3c( msg )
  12. ] )
  13. }
  14. )
  15. .then(step4);

但是对于表达异步流程控制来说有更好的选项,而且在代码风格上可能比长长的promise链更理想。我们可以使用在第三章中学到的generator来表达我们的异步流程控制。

要识别一个重要的模式:一个generator可以yield出一个promise,然后这个promise可以使用它的完成值来推进generator。

考虑前一个代码段,使用generator来表达:

  1. function *main() {
  2. try {
  3. var ret = yield step1();
  4. }
  5. catch (err) {
  6. ret = yield step1Failed( err );
  7. }
  8. ret = yield step2( ret );
  9. // step 3
  10. ret = yield Promise.all( [
  11. step3a( ret ),
  12. step3b( ret ),
  13. step3c( ret )
  14. ] );
  15. yield step4( ret );
  16. }

从表面上看,这个代码段要比前一个promise链等价物要更繁冗。但是它提供了更加吸引人的 —— 而且重要的是,更加容易理解和阅读的 —— 看起来同步的代码风格(“return”值的=赋值操作,等等),对于try..catch错误处理可以跨越那些隐藏的异步边界使用来说就更是这样。

为什么我们要与generator一起使用Promise?不用Promise进行异步generator编码当然是可能的。

Promise是一个可信的系统,它将普通的回调和thunk中发生的控制倒转(参见本系列的 异步与性能)反转回来。所以组合Promise的可信性与generator中代码的同步性有效地解决了回调的主要缺陷。另外,像Promise.all([ .. ])这样的工具是一个非常美好、干净的方式 —— 在一个generator的一个yield步骤中表达并发。

那么这种魔法是如何工作的?我们需要一个可以运行我们generator的 运行器(runner),接收一个被yield出来的promise并连接它,让它要么使用成功的完成推进generator,要么使用拒绝的理由向generator抛出异常。

许多具备异步能力的工具/库都有这样的“运行器”;例如,Q.spawn(..)和我的asynquence中的runner(..)插件。这里有一个独立的运行器来展示这种处理如何工作:

  1. function run(gen) {
  2. var args = [].slice.call( arguments, 1), it;
  3. it = gen.apply( this, args );
  4. return Promise.resolve()
  5. .then( function handleNext(value){
  6. var next = it.next( value );
  7. return (function handleResult(next){
  8. if (next.done) {
  9. return next.value;
  10. }
  11. else {
  12. return Promise.resolve( next.value )
  13. .then(
  14. handleNext,
  15. function handleErr(err) {
  16. return Promise.resolve(
  17. it.throw( err )
  18. )
  19. .then( handleResult );
  20. }
  21. );
  22. }
  23. })( next );
  24. } );
  25. }

注意: 这个工具的更丰富注释的版本,参见本系列的 异步与性能。另外,由各种异步库提供的这种运行工具通常要比我们在这里展示的东西更强大。例如,asynquence的runner(..)可以处理被yield的promise、序列、thunk、以及(非promise的)间接值,给你终极的灵活性。

于是现在运行早先代码段中的*main()就像这样容易:

  1. run( main )
  2. .then(
  3. function fulfilled(){
  4. // `*main()` 成功地完成了
  5. },
  6. function rejected(reason){
  7. // 噢,什么东西搞错了
  8. }
  9. );

实质上,在你程序中的任何拥有多于两个异步步骤的流程控制逻辑的地方,你就可以 而且应当 使用一个由运行工具驱动的promise-yielding generator来以一种同步的风格表达流程控制。这样做将产生更易于理解和维护的代码。

这种“让出一个promise推进generator”的模式将会如此常见和如此强大,以至于ES6之后的下一个版本的JavaScript几乎可以确定将会引入一中新的函数类型,它无需运行工具就可以自动地执行。我们将在第八章中讲解async function(正如它们期望被称呼的那样)。