彻底理解 thunk 函数与 co 框架

ES6 带来了很多新的特性,其中生成器、yield等能对js金字塔式的异步回调做到很好地解决,而基于此封装的co框架能让我们完全以同步的方式来编写异步代码。这篇文章对生成器函数(GeneratorFunction)及框架thunkify、co的核心代码做了比较彻底的分析。co的使用还是比较广泛的,除了我们日常的编码要用到使我们的代码逻辑更清晰易懂外,一些知名框架也是基于co实现的,比如被称为下一代的Nodejs web框架的koa等。

生成器函数

生成器函数是写成:

格式的函数,其本质也是一个函数,所以它具备普通函数所具有的所有特性。除此之外,它还具有以下有用特性:

  1. 执行生成器函数后返回一个生成器(Generator),且生成器具有throw()方法,可手动抛出一个异常,也常被用于判断是否是生成器;
  2. 在生成器函数内部可以使用yield(或者yield*),函数执行到yield的时候都会暂停执行,并返回yield的右值(函数上下文,如变量的绑定等信息会保留),通过生成器的next()方法会返回一个对象,含当前yield右边表达式的值(value属性),以及generator函数是否已经执行完(done属性)等的信息。每次执行next()方法,都会从上次执行的yield的地方往下,直到遇到下一个yield并返回包含相关执行信息的对象后暂停,然后等待下一个next()的执行;
  3. 生成器的next()方法返回的是包含yield右边表达式值及是否执行完毕信息的对象;而next()方法的参数是上一个暂停处yield的返回值。

下面用例子说明:

例1:

根据上面说过的第3条执行准则:“生成器的next()方法返回的是包含yield右边表达式值及是否执行完毕信息的对象;而next()方法的参数是上一个暂停处yield的返回值”,因为我们没有往生成器的next()中传入任何值,所以:var a = yield ‘a’;中a的值为undefined。

那我们可以将例子稍微修改下:

例2:

这个就比较清晰明了了,不再做过多解释。

关于yield*

yield暂停执行并只返回右值,而yield*则将函数委托到另一个生成器或可迭代的对象(如:字符串、数组、类数组以及ES6的Map、Set等)。举例如下:

arguments

Generator

thunk函数

在co的应用中,为了能像写同步代码那样书写异步代码,比较多的使用方式是使用thunk函数(但不是唯一方式,还可以是:Promise)。比如读取文件内容的一步函数fs.readFile()方法,转化为thunk函数的方式如下:

那什么叫thunk函数呢?

thunk函数具备以下两个要素:

  1. 有且只有一个参数是callback的函数;
  2. callback的第一个参数是error。

使用thunk函数,同时结合co我们就可以像写同步代码那样来写书写异步代码,先来个例子感受下:

是不是很酷?真的很酷!

其实,对于每次都去自己书写一个thunk函数还是比较麻烦的,有一个框架thunkify可以帮我们轻松实现,修改后的代码如下:

对于thunkify框架的理解注释如下:

代码并不复杂,看注释应该就能看懂了。

co框架

我们先将对核心部分加了注释的整个框架列出在下面,你可以先大概看下心里有个数,也可以在下面分析整个执行逻辑后回过头来细看:

下面,我们基于我们之前的例子对co的执行流程做一下分析。

我们的例子是:

首先,执行co()函数,内部除了缓存当前执行上下文环境、除generator函数之外的参数处理,主要返回一个Promise实例:

我们主要看这个Promise内部做了什么。

首先,判断co()函数的第一个参数是否是函数,是的话将除gen之外的参数传给该函数并返回给gen;在这里因为gen是一个生成器函数,所以返回一个生成器;

后面判断如果gen此时不是一个生成器,则直接执行Promise的resolve,其实就是将gen传回给:co().then(function(val){});里的val了;

我们这个例子gen是一个生成器,则继续往下执行。

后面我们就遇到了co的核心函数:onFulfilled。我们看下这个函数做了什么。

为了防止分心,里面错误的处理我们先暂时不理。

第一次执行该方法,res值为undefined,然后执行生成器的next()方法,对应我们例子里就是执行:

那么ret是一个对象,大概是这样:

然后将ret传给next函数。next函数是:

首先判断生成器内部是否已经执行完,执行完则将执行结果resolve出去。很明显我们例子里才执行到第一个yield,并没有执行完。没执行完,则将ret.value转化为一个Promise实例,我们这里是一个thunk函数,所以toPromise真正执行的是:

执行后其实就是直接返回了一个Promise实例。而这里面,也对fn做了执行,fn是:function(cb){},对应到这里,function(err, res){…}就是被传入到fn中的cb,第一个参数就是error对象,第二个参数res就是读取文件后数据,然后执行resolve,将结果传到下一个then方法的成功函数内,而在这里对应的是:

其实也就是onFulFilled的参数res。根据上面第三条执行准则,我们知道,res是被传入到生成器的next()方法里的,其实也就是对应co内生成器函数参数里的var a = yield readFile(‘a.txt’,{encoding:’utf8′});里的a的值,从而实现了类似于同步的变成范式。

这样,整个基于thunk函数的co框架编程也就理通了,其他的Promise、Generator、GeneratorFunction、Object、Array模式的类似,不再做过多分析。

理解了co的执行逻辑,我们就能更好的掌握其用法,对于后续使用koa等基于co编写的框架我们也能更快速地上手。

co的简版

为了更方便快捷的理解co的执行逻辑,在网络上还有一个简版的实现,如下:

但这个实现,仅支持yield后面是thunk函数的情形。使用示例:

会打印出:

希望能对理解和学习co的使用方法有很好的帮助。

以上。

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

1 5 收藏 1 评论

关于作者:abell123

相关文章

可能感兴趣的话题



直接登录
最新评论
跳到底部
返回顶部