初识generator
generator
是ES6的新特性,也是一种异步编程的解决方案。generator函数通过function*()
来表示,它相当于一个状态机,在内部通过yield来标识每一个状态。generator函数的返回值是一个指向内部状态的指针,我们将它命名为指针g。
在generator函数内部,凡是遇到yield标记的语句都会被阻塞。它们的控制权完全交给指针g,当指针g执行next时,相当于恢复generator的执行,相应的,next操作会返回一个包含value和done的对象。若当前generator执行完成,也就是之后没有yield语句了,value返回undefind,done返回true,告诉next执行完成。如果后面会被阻塞,则done返回false,value则为yield后面语句的值。next可被传入值,所传入的值将作为yield的返回值。具体执行流程可见下图:
注意一种特殊情况: 当generator函数内部有return时,返回{ value: return值, done: true }
,结束generator的执行。
从异步到同步
开头我们提到generator是异步编程的解决方案,那么generator是如何将异步改变为同步的?我们通过一些简明的generator实例来说明问题。
yield 数字、字符串、普通函数
|
|
对于数字、字符和普通函数而言,并没有异步同步的概念,需要清楚的是yield后表达式的值只是作为g.next()返回值中的value,不会直接作为yield的返回值。
yield Callback函数
|
|
一般的callback函数的形式是这样的:
|
|
直接在yield后面写这样的函数是丝毫没有作用的,异步依旧是异步。我们所需要做的是将callback返回给next().value,能从中获取异步结果,传给下一个next,这样yield的返回值也就拿到了异步的结果,此时,异步就不知不觉变同步了。
正如上面的readFile的示例一样,我们期望把一般的callback函数转化为以callback为单参数的函数,这就是thunk化。
|
|
这里自己写了一个简易实现,更详细的实现可参见node-thunkify源码
yield Promise
|
|
promise与callback异曲同工,返回一个promise作为value的值,通过reslove获取到异步结果,传给下一个next。
yield generator
利用yield*,来执行generator内嵌的generator
|
|
yield 数组
对于数组而言,会并行执行,等待数组中的所有成员均执行完后,在value中返回包含所有结果的数组。
|
|
co的实现
co的本质其实就是对于g.next()的执行和结果传递,yield后面会跟各种不同的数据结构(如: Promise、数组、对象、thunk函数等等),不同的数据结构对于g.next()的执行略微有些区别,co对这个过程进行了封装,让我们能专注于generator函数内部的逻辑编写。
co只接受一个参数,可能是generator function,即function* ()
,gen.apply执行,可能是generator,即function *()()
,执行指针,通过gen.next()判断。
|
|
控制generator执行部分,核心思想在于将gen.next的返回结果,传给next函数,首先判断若done为true,则结束整个generator,执行返回promise的resolve,传入最后的value值。若为false,对value值promise化,无论value是什么。value.then注入onFulfilled与onRejected,来执行下一次gen.next。
在generator中,一遇到错误就会通过返回的promise的reject对外抛出异常。
|
|
co最核心的地方就在于value的promise化,使yield后表达式返回值传递得到统一。
|
|
前三个都比较简单,如果obj未定义,返回undefined,如果本身就是promise,就不需要再转化了,如果是generator或是generator function,那么就在co的内部再执行co,这里可以关注一下是如何判断generator和generator function的。
对于co,我们必须使用thunk化后的function,我们只允许其有一个参数callback,但允许callback有多个参数,但第一个必须为error。
|
|
将数组中的所有值均promise化后执行,Promise.all会等待数组内所有promise均fulfilled、或者有一个rejected,才会执行其后的then。
|
|
注意在数组和对象中是可以出现一些普通值的,比如数字、对象等等,它们不会被toPromise promise化。
对象与数组也有相似之处,对象通过key进行遍历,对于每个被promise化好的value,都将其存储于promises中,最后Promise.all,生成results。
|
|
最后讲讲co.wrap,co.wrap主要是将co转变为一个返回promise的普通函数,可用于抽象,同时避免相同的co创建新的闭包。有一点切勿混淆,无论是co还是co.wrap, function* ()
都是可以传入参数的。
|
|
最近也在看redux-saga
和Koa
,它们就是基于generator的,等学习完后再进行总结和归纳。