异步编程之co——源码分析

系统 1866 0

异步编程系列教程:

  1. (翻译)异步编程之Promise(1)——初见魅力
  2. 异步编程之Promise(2):探究原理
  3. 异步编程之Promise(3):拓展进阶
  4. 异步编程之Generator(1)——领略魅力
  5. 异步编程之Generator(2)——剖析特性
  6. 异步编程之co——源码分析

如何使用co


大家如果能消化掉前面的知识,相信这一章的分析也肯定是轻轻松松的。我们这一章就来说说,我们之前一直高调提到的 co 库。 co 库,它用Generator和Promise相结合,完美提升了我们异步编程的体验。我们首先看看如何使用 co 的,我们仍旧以之前的读取Json文件的例子看看:

    
      // 注意readFile已经是Promise化的异步API 
co(function* (){
    var filename = yield readFile('hello3.txt', 'utf-8');
    var json = yield readFile(filename, 'utf-8');
    return JSON.parse(json).message;
}).then(console.log, console.error);
    
  

大家看上面的代码,甚至是可以使用同步的思维,不用去理会回调什么鬼的。我们 readFile() 得到 filename ,然后再次 readFile() 得到 json ,解析完json后输出就结束了,非常清爽。大家如果不相信的话,可以使用原生的异步api尝试一下, fs.readFile() 像上面相互有依赖的,绝对恶心!

我们可以看到,仅仅是在promise化的异步api前有个 yield 标识符,就可以使 co 完美运作。上一篇我们也假想过 co 的内部是如何实现的,我们再理(fu)顺(zhi)一次:

  1. 我们调用遍历器的 next() 得到该异步的promise对象
  2. 在promise对象的 then() 中的 resolve 对数据进行处理
  3. 把处理后的数据作为参数 res 传入 next(res) ,继续到下一次异步操作
  4. 重复2,3步骤。直到迭代器的 done: true ,结束遍历。

如果不清楚我们上面说过的Generator遍历器或promise对象的,可以先放一放这篇文章,从之前的几篇看起。

进入co的世界


获得遍历器

co的源码包括注释和空行仅仅才240行,不能再精简!我们抽出其中主要的代码来进行分析。

    
      function co(gen) {
  var ctx = this; // context
  
  // return a promise
  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.call(ctx); // 调用构造器来获得遍历器
    if (!gen || typeof gen.next !== 'function') return resolve(gen);
    
    //...下面代码暂时省略...
   })
}
    
  

这里我们需要关注的有两点:

  1. co函数最终返回的是一个Promise。
  2. 第6行代码,我们可以看到gen变量一开始就已经自身调用了。也就是gen从构造器变成了遍历器。

    遍历器开始遍历
    ---
    我们首先看看 co 内部的 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) + '"'));
    }
    
  

我们可以看到,ret参数有 done value ,那么ret肯定就是遍历器每次 next() 的结果。如果发现遍历器遍历结束的话,便直接return整个大Promise的 resolve(ret.value) 方法结束遍历。对了,此遍历器的 next() 和co的 next() 在这里是不一样的。当然你可以认为co将遍历器的 next() 又封装了一遍方便源码使用。

接着看,如果并没有完成遍历。我们就会对 ret.value 调用 toPromise() ,这里有知识点延伸,暂且先跳过,因为我们 一个 promise化的异步操作就是返回promise的。不知道大家get到point没?我就透漏一点,当是数组或对象时, co 会识别并支持多异步的并行操作,先不管~~

我们在保证我们调用异步操作得到的 value 是promise后,我们就会调用 value.then() 方法为promise的 onFulfilled() onRejected() 进行回调的绑定。也就是说,这段时间程序都是在干其他和遍历器无关的事的。遍历器没有得到遍历器的 next() 指令,就一直静静的等着。我们可以想到, next() 指令,必定是放在了那两个回调函数( onFulfilled onRejected )里。

自动运行

promise化的异步API是先绑定了回调方法,然后等待异步完成后进行触发。所以我们把遍历器继续遍历的 next() 指令放在回调中,就可以达到回调返回数据后再调用遍历器 next() 指令,遍历器才会继续下一个异步操作。

    
      	function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res); // 遍历器进行遍历,ret是此次遍历项
      } catch (e) {
        return reject(e);
      }
      next(ret); // ret.value is a promise
    }
    
  

我们看到第四行,通过调用遍历器的 next(res) ,再次启动遍历器得到新的遍历结果,再传入 co next() 里,重复之前的操作,达到自动运行的效果。这里需要注意一个地方,我们是通过向遍历器的 next(res) 传入 res 变量来实现将异步执行后的数据保存到遍历器里。

理解的关键

我相信我不可能说的很明白,让大家一下子就知道关键重点是哪个。我自己也是悟了不少时间的,最终发现那个可以使思路清晰的就是 Deferred 延迟对象。我在第二篇也有着重说过 Deferred 延迟对象,它最重要的一点就是,它是用来延迟触发回调的。我们先通过延迟对象的promise进行回调的绑定,然后在Node的异步操作的回调中触发promise绑定的函数,实现异步操作。当然这里也是如此,我们是把遍历器的 next() 指令延迟到回调时再触发。当然在 co 源码里是直接使用了ES6的promise原生对象,我们看不到 deferred 的存在。

所以我很早前就说了,promise对理解 co 至关重要。之前在promise上也花费了特别大的精力去理解,并分析原理。所以大家如果没有看之前的有关promise文章的,最好都回去看一看,绝对有好处!

co其他的内容


分析完 co 最关键的部分,接下来就是其他各种有用的源码分析。关于 thunk 转化为 promise 我就不说了,毕竟它也是被淘汰了的东西。那要说的东西其实就两个,一个是多异步并行,一个是将 co-generator 转化为常规函数。我们一个一个来讲:

多异步并行

之前也有提到过,就是我们需要对迭代对象的值进行 toPromise() 操作。这个操作顾名思义,就是将所有需要yield的值,通通转化为promise对象。它的源码就是这样的,并不能看到实质的东西:

    
      function toPromise(obj) {
  if (!obj) return obj;
  if (isPromise(obj)) return obj;
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  if (isObject(obj)) return objectToPromise.call(this, obj);
  return obj;
}
    
  

我们还记得在 co next() 函数里可以看到有一个注释是这样的:

'You may only yield a function, promise, generator, array, or object'

意思是,我们不仅仅只可以yield一个promise对象。function和promise我们就不说了,重点就是在array和object上,它们都是通过递归调用 toPromise() 来实现每一个并行操作都是promise化的。

数组Array

我们先看看相对简单的array的源码:

    
      function arrayToPromise(obj) {
  return Promise.all(obj.map(toPromise, this));
}
    
  

map是ES5的array的方法,这个相信也有人经常使用的。我们将数组里的每一项的值,再进行一次 toPromise 操作,然后得到全部都是promise对象的数组交给 Promise.all 方法使用。这个方法在promise文章的第二篇也讲过它的实现,它会在所有异步都执行完后才会执行回调。最后 resolve(res) res 是一个存有所有异步操作执行完后的值的数组。

对象Object

Object就相对复杂些,不过原理依然是大同小异的,最后都是回归到一个promise数组然后使用 Promise.all() 。使用Object的好处就是,异步操作的名字和值是可以对应起来的,来看看代码:

    
      function objectToPromise(obj){
  var results = new obj.constructor();
  var keys = Object.keys(obj); // 得到的是一个存对象keys名字的数组
  var promises = [];           // 用于存放promise
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var promise = toPromise.call(this, obj[key]);
    if (promise && isPromise(promise)) defer(promise, key);
    else results[key] = obj[key];
  }
  return Promise.all(promises).then(function () {
    return results;
  });

  function defer(promise, key) {
    // predefine the key in the result
    results[key] = undefined;
    promises.push(promise.then(function (res) {
      results[key] = res;
    }));
  }
}
    
  

第一个就是新建一个和传入的对象一样构造器的对象( 这个写法太厉害了 )。我们先获得了对象的所有的keys属性名,然后根据keys,来获取到每一个对象的属性值。一样是用 toPromise() 让属性值——也就是并行操作promise化,当然非promise的值就会直接存到results这个对象里。如果是promise,就会执行内部定义的 defer(promise, key) 函数。

所以理解defer函数是关键,我们看到是在defer函数里,我们才将当前的promise推入到promises数组里。并且每一个promise都是绑定了一个 resolve() 方法的,就是将结果保存到 results 的对象中。最后我们就得到一组都是promise的数组,通过 Promise.all() 方法进行异步并行操作,这样每个promise的结果都会保存到result对象相应的key里。而我们需要进行数据操作的也就是那个对象里的数据。

这里强烈建议大家动手模拟实现一遍 objectToPromise。

co.wrap(*generatorFunc)
---
下一个很有用的东西就是 co.wrap() ,它允许我们将 co-generator 函数转化成常规函数,我觉得这个还是需要举例子来表明它的作用。假设我们有多个异步的读取文件的操作,我们用co来实现。

    
      //读取文件1
co(function* (){
    var filename = yield readFile('hello1.txt', 'utf-8');
    return filename;
}).then(console.log, console.error);
//读取文件2
co(function* (){
    var filename = yield readFile('hello2.txt', 'utf-8');
    return filename;
}).then(console.log, console.error);
    
  

天啊,我仿佛又回到了不会使用函数的年代,一个功能一段函数,不能复用。当然 co.wrap() 就是帮你解决这个问题的。

    
      var getFile = co.wrap(function* (file){
    var filename = yield readFile(file, 'utf-8');
    return filename;
});

getFile('hello.txt').then(console.log);
getFile('hello2.txt').then(console.log);
    
  

例子很简单,我们可以将 co-generator 里的变量抽取出来,形成一个常规的Promise函数(regular-function)。这样子就无论是复用性还是代码结构都是优化了不少。

既然知道了怎么用,就该看看它内部如何实现的啦,毕竟这是一次源码分析。其实如果对函数柯里化(偏函数)比较了解,就会觉得非常简单。

    
      co.wrap = function (fn) {
  createPromise.__generatorFunction__ = fn; // 这个应该是像函数constructor的东西
  return createPromise;
  function createPromise() {
    return co.call(this, fn.apply(this, arguments));
  }
};
    
  

就是一个偏函数,借助于高阶函数的特性,返回一个新函数 createPromise() ,然后传给它的参数都会被导入到Generator函数中。

异步编程之co——源码分析


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论