generator是什么?
generator是ES6提供的一种异步编程解决方案,在语法上,可以把它理解为一个状态机,内部封装了多种状态。执行generator,会生成返回一个遍历器对象。返回的遍历器对象,可以依次遍历generator函数的每一个状态。同时ES6规定这个遍历器是Generator函数的实例,也继承了Genarator函数的prototype对象上的方法。
最简单的generator函数,其实它就是一个普通的函数,但是它有两个特征。第一就是function关键字与函数名之间有一个*号,其二就是函数体内使用yield表达式来遍历状态:
function* newGenerator(){
yield 'hello';
yield 'world';
return 'ending';
}
执行generator函数之后,该函数并不会立即执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。通常使用遍历器对象的next方法。使得指针移向下一个状态。每一次调用next()方法,内部指针就从函数头部或上一次停下里的地方开始执行,直到遇到下一个yield表达式位置,由此可以看出,generator是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
generator中的yield表达式
yield表达式在generator中是作为一个暂停标志,当碰到yield时,函数暂停执行,等到下一次next()执行时,函数才从当前yield位置开始执行。并且,yield表达式只能用在Generator函数里边;同时,yield如果后边带一个,则就是相当于一个for...of的简写形式,如果yield后边不带,则返回的是generator的值。
function* gen() {
yield 'hello';
yield* 'hello';
}
let f = gen();
console.log(f.next().value);
console.log(f.next().value);
console.log(f.next().value);
console.log(f.next().value);
console.log(f.next().value);
上述例子中的后四个next()函数,就会顺序的返回h e l l
generator中的next函数
通过next函数,可以执行对应的yield表达式,且next()函数还可以带参数,该参数可以作为上一次yield表达式的返回值,因为yield本身是没有返回值的,如果next()中不带参数,则yield每次运行之后的返回值都是为undefined;
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a');
// 1. a
genObj.next('b');
// 2. b
上述函数中,第一次运行next(),运行到第一个next()函数截止,第二个next运行时,传入的参数为'a';则运行到第二个yield地方截止,然后第一个yield运行的返回值为'a',依次类推,则得到上述结果。
另外,通过for...of可以循环generator中的所有状态,并且不需要使用next()函数。除了for...of循环以外,扩展运算符(...),解构赋值和Array.form方法内部调用的,都是遍历器接口。
generator生成的对象,还有其他一些函数,比如throw()用来抛出错误,return()用来定义返回值并终止generator的状态。
以上的三个方法在本质上其实是一样的,他们就是让generator恢复执行,并且使用不同的语句来替代yield语句。
- next()是将yield表达式替换成一个值
- throw()是将yield表达式替换成一个throw语句
- return()是将yield表达式替换成一个return语句
Generator与协程
协程可以理解为“协作的线程”或者“协作的函数”。协程既可以是单线程实现,也可以用多线程实现,前者是一种特殊的子例程,后者是一种特殊的线程。
协程有点像函数,又有点像线程,它的运行流程大致如下。
- 第一步,协程A开始执行
- 第二部,协程A执行到一半,进入暂停,执行权转移到协程B
- 第三步,(过了一段时间)协程B交换执行权
- 最后,协程A恢复执行
协程适合用于多任务运行环境,它与普通的线程很相似,都有自己的执行上下文,可以分享全局量。他们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都是处于暂停状态。
由于JavaScript是单线程,只能保持一个调用栈,引入协程之后,每一个任务可以保持自己的调用栈,这样就可以再抛出错误的时候找到原始的调用栈,不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束了。
Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。
如果将Generator函数当做协程,完全可以将多个需要互相协作的任务写成Generator函数,他们之间使用yield标识交换控制权。
Generator 函数执行产生的上下文环境,一旦遇到yield命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行next命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。
Generator函数的多种用途
- 可以使异步操作来实现同步化表达
- 控制流管理
- 部署Iterator接口
- 做为数据结构
上述的介绍中,我们看到了generator是什么,下边我们看一下,目前中,我们使用最多的,generator函数的异步调用。
异步编程对于单线程的JavaScript无疑是十分重要的(可以笼统的将异步定义为不连续的执行)。之前的文章中,我们也说过,JavaScript中对于异步的实现,就是回调函数。我们之前也有使用过promise去进行死亡回调的改良,promise来使回调嵌套的表现形式更好了些。其实呢generator函数也可以用来实现异步回调的嵌套。
整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。即为next方法还可以接收参数,向Generator函数体内输入数据。
Thunk函数
Thunk函数是自动执行Generator函数的一种方法。
对于以前的函数参数的求值计算,有两种计算方式,一种是传值调用,一种是传名调用。编辑器中的“传名调用”的实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体,这个临时函数就叫做Thunk函数。
JavaScript中使用的是传值调用,它的Thunk函数含义有所不同。再JavaScript中,替换的不是表达式,而是多参数函数的休整。
Thunk函数可以用于Generator函数的自动流程管理。即使Generator函数可以自动执行。
co模块
co模块用于Generator函数的自动执行。co函数返回一个Promise对象,因此可以用then方法添加回调函数。
使用了Generator函数之后,我们可以将多个异步嵌套的代码改为同步写异步,大大的简化了我们的代码量,以及代码的美观。
上边就是大概的ES中的Generator函数的介绍,我们可以使用Generator来实现 lazy evaluation,Iterator和实现异步调用同步的写法,但是其中我们面临的更多的还是generator的执行问题,下一篇我们来看一下generator是怎么实现async/await来控制异步。以及co模块的实现。