Generator函数实现异步编程,利用的是协程的思想。Generator函数可以将异步流程表示的很简洁,但是流程管理不方便,有两种方式进行Generator函数的自动流程化管理,一种是利用Thunk函数,另外一种是使用Promise对象,二者结合起来就是co模块。
 
传统方法 所谓异步,简单说就是一个任务不是连续完成的,被分成了两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
在JavaScript中,ES6之前,实现的异步编程的方法有四种:
回调函数 
事件监听 
发布/订阅 
Promise 
 
回调函数 所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到回过头重新执行这个任务的时候,就直接调用这个函数。
第二段所需要信息和错误对象,都必须通过参数的形式传递给回调函数,这是因为程序分为两段执行,当第一段执行后,==任务所在的上下文环境就已经结束了==。在这之后的任务信息和抛出的错误,原来的上下文环境已经无法捕获,所以只能当做参数传入。
Promise 回调函数本身没有问题,但是当多个回调函数存在的时候,会出现“回调地狱”,形成强耦合,只要有一个操作需要修改,它的上下层函数都要跟着修改
1 2 3 4 5 fs.readFile (fileA, 'utf-8' , function  (err, data ) {   fs.readFile (fileB, 'utf-8' , function  (err, data ) {        }); }); 
 
Promise对象就是为了解决这个问题而提出的,它不是新的语法功能,而是一种新的写法,将回调函数的嵌套改为链式调用:
1 2 3 4 5 6 7 8 9 10 11 12 var  readFile = require ('fs-readfile-promise' );readFile (fileA).then (function  (data ) {   console .log (data.toString ()); }) .then (function  ( ) {   return  readFile (fileB); }) .catch (function  (err ) {   console .log (err); }); 
 
Promise的问题是代码冗余,很多的then导致语义不清楚
Generator函数 协程的 Generator 函数实现 Generator函数实现异步编程,利用的是协程的思想:
协程A开始执行 
协程A执行到一半,进入暂停,执行权转移到协程B 
一段时间后,协程B交换执行权 
协程A恢复恢复执行 
 
协程A就是异步任务,分为了多段执行
1 2 3 4 5 function * asyncJob ( ) {     var  f = yield  readFile (fileA);    } 
 
上面的asyncJob就是一个协程,关键就在于yield命令,当程序执行到此处,asyncJob将执行权交给其他协程
整个Generator函数就是一个异步任务的容器,程序需要暂停的地方都需要使用yidld表达式
Generator之所以能够成为异步编程的旺盛解决方法,除了可以暂停执行和恢复执行之外,还因为Generator函数体内外的==数据交换和错误处理机制==。
上一篇笔记详细学习过Generator的基础知识了,看这里 。
异步任务的封装 看一个例子:
1 2 3 4 5 6 7 var  fetch = require ('node-fetch' );function * gen ( ){  var  url = 'https://api.github.com/users/github' ;   var  result = yield  fetch (url);    console .log (result.bio ); } 
 
执行折断代码的方法:
1 2 3 4 5 6 7 8 var  g = gen ();var  result = g.next ();result.value .then (function (data ){   return  data.json (); }).then (function (data ){   g.next (data); }); 
 
Generator函数将异步流程表示的很简洁,但是流程管理不方便,即何时执行第一段、何时执行第二段
Thunk 参数的求值策略 求值策略关注的是函数的参数到底应该何时求值
有两种求值策略,一种是传值调用,即在进入函数体之前就进行计算,另一种是传名调用,即只将表达式传入函数体,旨在用到的时候求值。
传值调用有可能造成性能的浪费。
Thunk函数的含义 编译器的传名调用,是将参数放到一个临时函数中,再将这个函数传入函数体,这个临时函数叫做Thunk函数
1 2 3 4 5 6 7 var  thunk = function  ( ) {  return  x + 5 ; }; function  f (thunk ) {  return  thunk () * 2 ; } 
 
Thunk函数是传名调用的一种实现,用来替换某个表达式
JavaScript中的Thunk的函数 JavaScript中的Thunk函数替换的不是表达式,而是多参数函数,将多参数函数替换为只接受一个回调函数作为参数的==单参数函数==。
1 2 3 4 5 6 7 8 9 10 11 12 fs.readFile (fileName, callback); const  Thunk  = fileName => {  return  function (callback ) {     fs.readFile (fileName, callback);   } } const  readFileThunk = Thunk (fileName);readFileThunk (callback)
 
任何函数,只要参数有回调函数,就能写成Thunk函数的形式,简单的Thunk函数转换器:
1 2 3 4 5 6 7 const  Thunk  = function  (fn ) {  return  function (...args ) {     return  fucntion (callback ) {       return  fn.call (this , ...args, callback)     }   } } 
 
使用:
1 2 const  readFileThunk = Thunk (fs.readFile );readFileThunk (fileA)(callback)
 
Generator函数的流程管理 Thunk函数可以用于Generator函数的自动流程管理,下面的Generator函数中封装了两个异步操作:
1 2 3 4 5 6 7 8 9 10 var  fs = require ('fs' );var  thunkify = require ('thunkify' );var  readFileThunk = thunkify (fs.readFile );var  gen = function * (){  var  r1 = yield  readFileThunk ('/etc/fstab' );   console .log (r1.toString ());   var  r2 = yield  readFileThunk ('/etc/shells' );   console .log (r2.toString ()); }; 
 
在使用Thunk函数管理之前,看一下如何手动执行上面这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const  g = gen ();const  r1 = g.next ();r1.value (funciton (err, data ) {   if  (err) {     throw  err;   }   const  r2 = g.next (data);   r2.value (function (err, data ) {     if  (err) {       throw  err;     }     g.next (data)   }) }) 
 
为什么能够在r1.value里面传入一个函数呢?r1.value是第一个yield的结果,也就是readFileThunk('/etc/fstab')的结果,它是一个Thunk化的函数,返回值仍是一个函数,参数是回调函数:
1 2 3 4 5 6 const  thunk1 = r1.value ;thunk1 (function (err, data ){   })) 
 
通过上面的代码可以发现,Generator函数的执行过程,就是将同一个回调函数返回传入next方法返回值的value属性。
这使得我们可以通过递归来自动完成这个过程
Thunk函数的自动流程化管理 Thunk函数的真正的威力,就在于可以==自动执行==Generator函数。下面是一个基于Thunk函数的Generator执行器
1 2 3 4 5 6 7 8 9 10 11 function  run (fn ) {  const  gen = fn ();      function  next (err, data ) {     const  result = gen.next (data);     if  (result.done ) {       return ;     }     result.value (next)   } } 
 
有了这个执行器执行Generator函数的时候,不管内部有多少个异步操作,直接将Generator函数传入run函数即可(但是前提==每一个异步操作都要是Thunk函数==)
1 2 3 4 5 6 7 8 var  g = function * (){  var  f1 = yield  readFileThunk ('fileA' );   var  f2 = yield  readFileThunk ('fileB' );      var  fn = yield  readFileThunk ('fileN' ); }; run (g);
 
Thunk函数并不是Generator函数自动执行的唯一方案,因为自动执行的关键是,必须==有一种机制,自动控制Generator函数的流程,接受和交换程序的执行权==。
Promise对象也可以代替回调函数做到这一点。
co模块 co模块让你不用编写Generator函数的执行器:
1 2 3 4 5 6 7 8 9 10 const  co = require ('co' );var  gen = function * () {  var  f1 = yield  readFile ('/etc/fstab' );   var  f2 = yield  readFile ('/etc/shells' );   console .log (f1.toString ());   console .log (f2.toString ()); }; co (gen).then (() =>  {console .log ('执行完成' )})
 
co函数返回一个Promise对象,当Generator函数执行完,可以用then方法添加回调函数。
co模块的原理 co模块将两种自动执行器(Thunk函数和Promise对象)包装成一个模块,使用co模块的前提条件是,Generator函数的yield命令后面,只能是Thunk函数或者Promise对象(或者数组或对象的成员全都是Promise对象)
基于Promise对象的自动执行 同样的例子,将fs模块的readFile方法包装成为一个Promise对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const  fs = require ('fs' );const  readFile = function  (fileName ) {  return  new  Promise ((resolve, reject ) =>  {     fs.readFile (fileName, (err, data ) =>  {       if  (err) {         reject (err)       }       resolve (data)     })   }) }; const  gen = function * () {  const  f1 = yield  readFile ('/etc/filaA' );   const  f2 = yield  readFile ('/etc/filaB' );   console .log (f1.toString ());   console .log (f2.toString ()); } 
 
然后手动执行上面的函数:
1 2 3 4 5 6 7 var  g = gen ();g.next ().value .then (function (data ){   g.next (data).value .then (function (data ){     g.next (data);   }); }); 
 
实际上手动执行就是用then方法,层层添加回调函数(原理和前面的基于Thunk函数的自动执行器类似):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function  run (gen ){  var  g = gen ();   function  next (data ){     var  result = g.next (data);     if  (result.done ) return  result.value ;     result.value .then (function (data ){       next (data);     });   }   next (); } run (gen);
 
co模块的源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 function  co (gen ) {  var  ctx = this ;      return  new  Promise (function (resolve, reject ) {                    if  (typeof  gen === 'function' ) {       gen = gen.call (ctx);     }     if  (!gen || typeof  gen.next  !== 'function' ) {       return  resolve (gen);     }                    onFulfilled ();     function  onFulfilled (res ) {       var  ret;       try  {         ret = gen.next (res);       } catch  (e) {         return  reject (e);       }       next (ret);     }               function  next (ret ) {              if  (ret.done ) {         return  resolve (ret.value );       }                     var  value = toPromise.call (ctx, ret.value );                     if  (value && isPromise (value)) {                           return  value.then (onFulfilled, onRejected);       }                            return  onRejected (         new  TypeError (           'You may only yield a function, promise, generator, array, or object, '            + 'but the following object was passed: "'            + String (ret.value )           + '"'          )       );     }   }); } 
 
参考