7

之前我在关于Promise的文章中提到了co这个库。在这篇文章里,我将写一写自己对它的认识。

Trust me,用了co库,你不想用别的,来它半斤异步调用你一口能吃仨。

但是我对Tj大神的co库源码谈不上深入理解。所以,如有乱讲,欢迎指正。

我这里默认读者对PromiseGenerator有一定的认识。

先安利自己写的两篇关于Promise的文章:

下面我就来谈谈co这个牛逼的库。

ES7 async/await

干嘛,我们不是讲ES6么,怎么跳到ES7了?

因为co要做的事情,就是ES7的async/await要做的事情。
也就是说,这种解决异步的思路,已经在ECMA标准的考虑之中了。将来我们浏览器的JS引擎就可以原生实现这件事而不是通过JavaScript代码模拟。要知道,引擎的实现和代码的实现那是完全两码事。

一点题外话

多一句嘴:有些同学混淆了ECMA标准、引擎支持和代码实现的联系。

这里引用老赵在知乎里面回答问题时说的一句话:

ES7是个标准,定义的是what to do不是how to do,为什么好多人还是搞不清这两者的区别。

ECMAScript定义了一些JavaScript语言层面要做的事情,这是一个标准。之所以要制定这个标准,是为了防止浏览器各自为政而出现JS引擎对同一行代码的解释出现不同的情况。

也就是说,ECMA制定标准,我们就可以按照这个标准来写JavaScript代码。写好的JavaScript代码由浏览器的JS引擎来解释,最终变成计算机能读懂的代码来执行。


async/await

上代码:

var foo = function(){

    return new Promise(resolve => {
        // 异步操作之后
        resolve('OK');
    });
}

async funtion bar(){
    
    var result = await foo();
    
    console.log(result); 
    
}

bar(); // ==> 打印'OK'

我们注意到,这段代码用了两个新的关键字asyncawait。而且有两件神奇的事情发生了:

  1. bar函数中包含了一个返回Promise对象的语句,而且Promise中存在异步代码。但是这条语句接下来的语句明显是等待Promise对象中的代码异步执行完毕之后才执行的。(否则不会得到异步之后的值)

  2. Promise对象resolve的值,并没有在then中进行处理,而是直接作为返回值返回到Promise对象外面了.

这就是async/await的魔法。在函数前面加上async关键字之后,内部的代码会识别await关键字。此时假设await后面的语句返回一个Promise对象,那么执行的代码将会等待,直到Promise对象变为resolve状态。并且Promise对象中resolve的值将直接作为await语句的返回值返回。然后再执行await语句之后的语句。

从此我们就可以无痛的撸异步代码,妈妈再也不用担心回调金字塔的出现和异步流程逻辑搞不定的情况了!

另一个奇妙的事情就是,率先支持这一特性的浏览器居然是微软的Edge。大概是因为C#语言早就出现async/await,并且TypeScript也支持这一特性的缘故吧。

co

我们希望所有的浏览器都及早支持这一特性。但是值得欣喜的一点就是,虽然V8还没有支持,Tj大神早就利用Generator的方式实现了一个ES6版本的async/await!(膜拜脸)

co函数形式

同样是上面的逻辑,我们用co实现一次:

// 首先我们需要将co引入,假设我们使用commonJS的方式  

const co = require('co');

var foo = function(){

    return new Promise(resolve => {
        // 异步操作之后
        resolve('OK');
    });
}

co(function* (){
    
    var result = yield foo();
    
    console.log(result); 
    
}); // ==> 打印'OK'

我们看到,co函数接收一个Generator生成器函数作为参数。执行co函数的时候,生成器函数内部的逻辑像async函数调用时一样被执行。不同之处只是这里的await变成了yield

简单版本的co代码

要实现以上的逻辑,结合Generator的特性,co函数应该:

  • 在函数体内将Generator生成器函数执行并生成生成器实例(在此命名为gen),然后通过gen.next方法的调用,不断执行生成器函数内部的代码。

  • 执行next方法之后,返回的Promise在生成器函数执行环境之外执行,并取出resolve值,作为返回值作为next方法的参数返回到Generator执行环境中。

基于以上两点,我们可以大体实现一个简化版的co,代码如下:

const co = function(genFunc){  
    const gen = genFunc(); // 得到生成器实例  
    
    const deal = (val) => {
        
        const res = gen.next(val); 
        
        // 这里处理了异步逻辑,
        // 在回调中去递归,不断执行next
        // 这样就将resolve的值传回了Generator
        res.value.then(result => deal(result));
        
    }
    
    deal(); // 第一次触发递归
}

去掉括号等等,只有短短六行代码。

more

原理性的东西大约就是这样了。但是co做的不止这些。

  1. 之前coyield后的语句并不支持Promise对象,而是一个特殊的函数,叫做thunk。目前co二者都支持。
    此处我并不打算重复性解释thunk版本,因为原理性的东西实现起来是差不多的。

  2. co函数是有返回值的,也是一个Promise对象。

    • 当生成器函数内的逻辑执行完毕且没有错误之后,这个Promise对象(co返回值)变为resolve状态,且将生成器的返回值作为resolve出来的值。

    • 若生成器函数内返回一个Promise对象,那么co函数返回值就是这个Promise对象。

    • 若生成器函数抛出了错误,那么这个错误作为reject出来的值,将Promise对象的状态变为reject

    这样我们就可以将错误放进其返回值的.catch方法中统一处理。

  3. 在生成器函数内部,我们也可以使用try...catch语句获取错误对象。

  4. 生成器的yield后面可以跟一个元素值为Promise对象的数组,这个数组内Promise对象内的异步逻辑将并发执行,并返回一个数组。(类似于Promise.all方法)

  5. 假设生成器执行之前需要从外部传入参数,co库提供了一个方法:

  var fn = co.wrap(function* (val) {
  
     return yield Promise.resolve(val);

  });

  fn(true).then(function (val) {

  });

结束

以上是一点微小的见解。谢谢指正。


samchowgo
327 声望22 粉丝

有一咻咻故事的男同学。


« 上一篇
再谈Promise