在 ES6 中,对于异步问题的处理其实笼统的来说提供了三种解决方案,分别是
Promise
,Generator
,async/await
。但这三种方案并不是各自独立,而是互相关联,互相依赖的。它们也都有各自的一些应用场景和优缺点。这篇博客就主要来介绍一下
Generator
。OK,Let’s begin。
Generator 概述
首先,Generator
是ES6中新推出的一种函数的叫法,它是一种全新的函数类型。
它与JS
中其他函数的主要区别在于,它是一种可以在运行时暂停的函数。一般来说,在JS
中,当一个函数运行起来时,我们是无法中断它的,归因于JS
的单线程和事件循环机制,我们在以前是没法做到将一个正在运行的函数暂停去做一些其他事情,然后在需要时回来继续运行这个函数的。
而Generator
函数,正是为我们提供了这样一个实现,让我们可以在函数代码执行过程中,一次或多次暂停,并在将来的某个时刻继续执行。我们来仔细思考一下,对于普通的函数来说,我们在调用函数时向函数传入参数,并等待函数执行完毕为我们return
一个最终值。那么对于Generator
函数来说,我们在每次暂停时,都可以返回一个值,然后在启动时,再传入一个值。嗯,听起来还蛮振奋人心的函数内外部双向通信,用来处理异步问题,实在是再合适不过了啊。
generator 函数语法
首先,我们来看一下generator
函数是如何书写的。
generator
函数的声明语法跟普通函数的声明语法大致相同,不同的是,它多一个*
号。如下:
1 | /*以下两种generator函数声明都是合法的,但推荐使用第一种,比较明了*/ |
这个*
号,就是用来标示函数是一个generator
函数的。
如果接触过python
的话,会发现ES6
的generator
函数和python
的生成器函数十分相似,都是通过 yield
来在函数内部暂停函数的执行,并传递值到函数外去,再通过next()
传递值给yield
表达式,并继续函数的执行。(只是顺便一提,没了解过python
的同学可以当做没看见。。
我们先来看一个简单的 generator 函数的例子,通过这个简单的例子来对generator
函数做一个直观的认识。
1 | function *first(){ |
通过上面的函数,我们可以看到,我们定义的generator
函数有以下几个特别的地方:
first()
并不会执行first
函数,也不会返回函数运行结果,而是返回一个指向函数内部状态的指针对象,也就是一个Generator
对象。- 当我们执行调用
generator
函数返回的对象的next()
方法后,函数才开始执行,执行到yield
关键字时暂停,并返回一个对象,这个对象的value
值为yield
传递出的值,done
属性是函数的状态,当函数执行完毕时,它会从false
置为true
。
yield
大家generator
函数的关键语句——yield
。我们就来具体解释一下 yield
。
yield
的汉语意思就是产出,从上面的例子中也很好理解,就是产出一个形如{value: xxx, done:boolean}
的对象。
在generator
函数运行过程中,当遇到yield
语句时,就会暂停函数,并将yield
后的值作为next()
所返回对象的value
值。如果yield
表达式不返回值的话,value
就为undefined
。
再次调用next()
方法,函数继续从上次暂停的地方执行,直到再次遇到yield
,以此类推。
如果从上次暂停的位置后面没有yield
,函数就会一直执行下去,直到函数结束将next
返回对象的done
值置为true
。
值得注意的是,如果函数有return
语句的话,return
语句的返回值会作为next()
调用返回对象的最后一个value
,并结束函数,后续的next
调用,会一直返回undefined
。
关于yield
,还有以下两点需要注意:
yield
只能用在generator
函数中,不能用在普通函数中,因此它也不能用在那些以函数作为参数的方法中,例如forEach
,map
等方法。yield
如果用在表达式中参加运算,必须加括号。形如下:JavaScript 1
2
3function *second(){
let x = 1 + (yield 2)
}
双向通信
在上面我们提到了,yield
表达式可以暂停generator
函数的运行,并将其后的值传递出去。那么我们如何向yield
表达式传递值呢?也就是说,generator
函数是如何实现函数内外部的双向通信的呢?
看下面例子:
1 | function *foo(){ |
从上面代码我们可以看到,x的值在函数结束时为2。
通过以上代码我们就可以清楚的知道generator
函数如何实现双向通信了。
- 遇到
yield
表达式时,暂停函数,并将其后的值传递出去 - 在下一个
next
调用时,从暂停的yield
处继续运行,并将next
携带的参数作为此处yield
表达式的最终计算结果值。 - 如果
next()
没有传递参数给yield
表达式的话,默认yield
表达式返回值为undefined
。
形象的来说,yield
就像是在管道一样的函数上开了一个口子,在运行到它所在位置时,从这个口子会冒出一个东西,我们把冒出的东西拿到后,还可以通过这个口子,再往里面塞进去一个东西。而next
,就是用来使我们能够合乎规则和秩序的取到口子传递出的东西再塞东西进去。
看明白上面的例子后,我们再通过下面这个复杂一些的例子,更详细的加深一下理解。
1 | function *bar(x){ |
上面这个例子对于初次接触generator
的同学可能有些难以理解,多看几次^O^.
异常捕捉
我们可以通过next()
方法向generator
函数内传递数据,那么当然,像promise
对象的数据和错误处理配套的一样,我们也可以向generator
中传递异常。
来看代码:
1 | function *g(){ |
当然,在实际使用中,我们最好应该向throw
中传递一个Error
对象的实例。此处传递一个字符串的行为不可取,只是为了方便演示。
generator
对象的throw
方法只能在generator
函数启动后才能传递错误进去,很好理解,我们当然必须首先使用一次next
方法启动函数才能抛出异常啊。抛出的异常如果在内部未被捕获,就会扩散到throw
调用的位置,还没被捕获的话,程序就会被异常终止。
generator对象的return方法
generator
对象,除了我们上面提到的next
和throw
两个方法外,还有一个return
方法。它的作用是向generator
函数内部传递一个值,并结束generator
函数的运行。
如下:
1 | function *gen(){ |
我们在上面提到,generator
函数内部的return
会将函数结束,并将return
作为最后一个非undefined
的value
返回值。generator
对象的return
方法,作用跟函数内部的return
是一样的。
现在我们可以来总结一下next
,throw
,return
这三个方法:
- 这三个方法都是用于在
generator
函数从 上个暂停的yield
处启动时,向yield
表达式传递值作为其表达式结果的方法。 next
方法传递一个普通值作为yield
表达式的结果,相当于将yield
表达式替换为一个普通值throw
方法传递一个抛出的错误作为yield
表达式的结果,相当于将yield
表达式替换为一个throw
语句return
方法同样传递一个普通值作为yield
表达式的结果,但它会结束函数的运行,相当于将yield
表达式替换为一个return
语句。
使用generator函数解决异步问题
上面讲了一大堆关于generator
函数的特性和方法,但大家对于它具体怎么应用在一些异步操作中还是有些疑惑。
首先我们来思考一下,异步问题,归根结底就是有的操作,需要耗费比较长的时间,我们又不想在这些操作进行的时候一直等着它返回结果。我们想要的理想状态是,当这些操作进行时,我们可以先做一点别的事情,等操作完成了,我们再去得到结果就行。
generator
函数就正好符合我们的要求。我们可以将异步操作包装在generator
函数中,当异步操作进行时,函数暂停,我们可以去做其他的一些事情。当我们需要获得异步操作结果时,再使用next
方法,取到结果并进行处理。这样看来,generator
函数很适合来进行异步操作。
我们看下面的代码:
1 | //假设我们有一个耗时的操作 |
可以看出,通过generator
函数来进行异步处理,其实就是构造一个generator
函数,使用yield
来启动异步操作,并在异步操作中将结果再通过next()
方法传递回generator
函数,从而完成异步操作。
很多异步操作都是通过回调函数来传递操作结果的,那我们如果想使用generator
函数来对他们进行处理,就必须在他们的回调函数里使用next
方法,拿最常见的Ajax
举例。
1 | funciton req(){ |
那大家就可能有这样一个问题了,我为什么不直接在Ajax
的回调函数里对结果进行处理,还要费那么大麻烦把结果传递回generator
函数里干嘛。
额,单纯这样用generator
函数是没什么用,在多个异步操作时,我们要在每个异步操作的回调里使用next传递结果,到处的回调里都是next
,显得十分愚蠢。
于是就有了这样的一个解决方案,我们通过Thunk
函数,将形如$.get(url, callback)
类型的异步请求形式,转换为 aGet(url)(callback)
形式的调用,这样,我们就可以在每次 next
的返回值的value
中自定义回调函数,从而简化代码。如下:
1 | //首先通过thunk函数,将 $.get(url,cb) 转换为 aGet(url)(cb)的调用形式 |
这种方式第一是写起来比较麻烦,难以理解,不够清晰明了。第二就是需要引入额外的thunk
相关库来对各种有回调函数的异步API
进行包装,增加了使用成本。
对于那些不使用回调函数,而是返回promise
对象的异步API
,使用generator
函数来进行处理其实是一样的步骤,只不过将回调函数中的next
调用换到了then
中。
当然,generator
函数有一些相关的库,例如co
库,可以用来降低我们在回调函数和then
中使用next
来进行异步操作结果传递的麻烦程度,但同样的,也会增加我们的使用成本,降低多人协作开发中的容错率。引入相关的类库来改变项目中的所有异步操作处理步骤,可能对于一些大型项目来说,是需要慎重考虑的事情。
总结
相比于promise
的原生支持和简明的用法,generator
函数略显鸡肋,在ES7
推出的async/await
这个终极异步操作处理方案面前,generator
函数就更有些尴尬了。
如果希望了解更多关于generator
函数的知识,仍旧推荐阅读阮一峰老师的ES6入门
,关于generator
函数的一些细节和异步操作处理的更多应用,都介绍的比较详细。赠送飞机票一张
好了,对于generator
函数的介绍,就到这里了。谢谢阅读。