三十六、谈谈你对es6中的Generator函数的认识

811 阅读10分钟

一、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交出执行权,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.from方法内部调用的,都是遍历器接口。

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函数的应用

1、协程

协程可以理解成多线程间的协作,比如说A,B两个线程根据实际逻辑控制共同完成某个任务,A运行一段时间后,暂缓执行,交由B运行,B运行一段时间后,再交回A运行,直到运行任务完成。对于JavaScript单线程来说,我们可以理解为函数间的协作,由多个函数间相互配合完成某个任务。

下面我们利用饭店肚包鸡的制作过程来说明,熊大去饭店吃饭,点了只肚包鸡,然后就美滋滋的玩着游戏等着吃鸡。这时后厨就开始忙活了,后厨只有一名大厨,还有若干伙计,由于大厨很忙,无法兼顾整个制作过程,需要伙计协助,于是根据肚包鸡的制作过程做了如下的分工。

肚包鸡的过程:准备工作(宰鸡,洗鸡,刀工等)->炒鸡->炖鸡->上料->上桌

大厨很忙,负责核心的工序:炒鸡,上料

伙计负责没有技术含量,只有工作量的打杂工序:准备工作,炖鸡,上桌


//大厨的活

function* chef(){

console.log("fired chicken");//炒鸡

yield "worker";//交由伙计处理

console.log("sdd ingredients");//上料

yield "worker";//交由伙计处理

}

//伙计的活

function* worker(){

console.log("prepare chicken");//准备工作

yield "chef";//交由大厨处理

console.log("stewed chicken");//炖鸡

yield "chef";//交由大厨处理

console.log("serve chicken");//上菜

}

var ch = chef();

var wo = worker();

//流程控制

function run(gen){

var v = gen.next();

if(v.value =="chef"){

run(ch);

}else if(v.value =="worker"){

run(wo);

}

}

run(wo);//开始执行

我们来分析下代码,我们按照大厨和伙计的角色,分别创建了两个Generator函数,chef和worker。函数中列出了各自角色要干的活,当要转交给其他人任务时,利用yield,暂停执行,并将执行权交出;run方法实现流程控制,根据yield返回的值,决定移交给哪个角色函数。相互配合,直到完成整个过程,熊大终于可以吃上肚包鸡了。

我们执行看下效果,与工序保持一致。

2、异步编程

Generator函数,官方给的定义是"Generator函数是ES6提供的一种异步编程解决方案"。我认为它解决异步编程的两大问题

  • 回调地狱

  • 异步流控

那什么是异步的流控呢,简单说就是按顺序控制异步操作。下面我们就从工序的角度重新实现肚包鸡实例(上面的肚包鸡制作实例,是从角色的角度,这种模式其实并不是最佳的),每个工序都是可认为异步的过程,工序之间又是同步的控制(上一个工序完成后,才能继续下一个工序),这就是异步流控。

普通方法实现肚包鸡的制作过程:


setTimeout(function(){

console.log("prepare chicken");

setTimeout(function(){

console.log("fired chicken");

setTimeout(function(){

console.log("stewed chicken");

....

},500)

},500)

},500);

用setTimeout方法来模拟异步过程,这种层层嵌套就是回调地狱,就是回调地狱,Promise就是解决这种回调的解决方案,有兴趣的可以作为练习,用Promise修改这个例子。

我们用Generator来实现:


//准备

function prepare(sucess){

setTimeout(function(){

console.log("prepare chicken");

sucess();

},500)

}

//炒鸡

function fired(sucess){

setTimeout(function(){

console.log("fired chicken");

sucess();

},500)

}

//炖鸡

function stewed(sucess){

setTimeout(function(){

console.log("stewed chicken");

sucess();

},500)

}

//上料

function sdd(sucess){

setTimeout(function(){

console.log("sdd chicken");

sucess();

},500)

}

//上菜

function serve(sucess){

setTimeout(function(){

console.log("serve chicken");

sucess();

},500)

}

//流程控制

function run(fn){

const gen = fn();

function next() {

//返回工序函数的句柄给result

const result = gen.next();

if (result.done) return;//结束

// result.value就是yield返回的值,是各个工序的函数

result.value(next);//next作为入参,即本工序成功后,执行下一工序

}

next();

};

//工序

function* task(){

yield prepare;

yield fired;

yield stewed;

yield sdd;

yield serve;

}

run(task);//开始执行

我们来执行下这个过程,按照我们既定的工序顺序,每隔500ms打印如下内容。

我们分析下执行过程:

  • 每个工序对应一个独立的函数,在task中组合成工序列表,执行时将task作为入参传给run方法。run方法实现工序的流程控制。

  • 首次执行next()方法,进入next方法体内。

const result = gen.next();gen为传入的task函数,执行yield prepare,并将prepare函数对象句柄返回给result(注意:此时并没有执行prepare函数);

if (result.done) return;返回是否是结束状态,由于是第一步,为false跳过。

result.value(next);result.value即是prepare函数对象,next方法作为入参传入。执行result.value(next),其实就是执行prepare(next);prepre执行完成后,继续调用其入参的next(success())方法,即下一步工序,

3、以此类推,直到最后一步完成后,result.done为ture,结束整个工序。

从上面例子看,task方法将各类工序"扁平化",解决了层层嵌套的回调地狱;run方法,使各个工序同步执行,实现了异步流控。

六、Generator函数的面试题

1、解决AJAX的异步回调嵌套


ajax('http://url-1', data1, function (err, result) {

if (err) {

return handle(err);

}

ajax('http://url-2', data2, function (err, result) {

if (err) {

return handle(err);

}

ajax('http://url-3', data3, function (err, result) {

if (err) {

return handle(err);

}

return success(result);

});

});

});

将以上代码用generator函数改成同步执行方式:


try {

r1 = yield ajax('http://url-1', data1);

r2 = yield ajax('http://url-2', data2);

r3 = yield ajax('http://url-3', data3);

success(r3);

}

catch (err) {

handle(err);

}

2、用generator函数生成一个自增的ID


var current_id = 0;

function next_id() {

current_id ++;

return current_id;

}

将以上代码用generator函数进行实现:


function* next_id() {

for (id = 1; ; id++) {

yield id;

}

}

七、参考资料