一、什么是Generator 函数
先看下面的Generator函数,
function* helloGenerator() {
console.log("this is generator");
}
这个函数与普通的函数区别是在定义的时候有个*,我们来执行下这个函数。
function* helloGenerator() {
console.log("this is generator");
}
helloGenerator();//没有执行
我们发现,并没有像普通的函数一样,输出打印日志。我们把代码改成下面:
function* helloGenerator() {
console.log("this is generator");
}
var h = helloGenerator();
h.next();
这个时候如期的打印了日志,我们分析下,对于Generator函数,下面的语句
var h = helloGenerator();
仅仅是创建了这个函数的句柄,并没有实际执行,需要进一步调用next(),才能触发方法。
function* helloGenerator() {
yield "hello";
yield "generator";
return;
}
var h = helloGenerator();
console.log(h.next());//{ value: 'hello', done: false }
console.log(h.next());//{ value: 'generator', done: false }
console.log(h.next());//{ value: 'undefined', done: true }
这个例子中我们引入了yield这个关键字,分析下这个执行过程
(1)创建了h对象,指向helloGenerator的句柄,
(2)第一次调用nex(),执行到"yield hello",暂缓执行,并返回了"hello"
(3)第二次调用next(),继续上一次的执行,执行到"yield generator",暂缓执行,并返回了"generator"。
(4)第三次调用next(),直接执行return,并返回done:true,表明结束。
经过上面的分析,yield实际就是暂缓执行的标示,每执行一次next(),相当于指针移动到下一个yield位置。
总结一下,Generator函数是ES6提供的一种异步编程解决方案。通过yield标识位和next()方法调用,实现函数的分段执行。
二、Generator 函数与迭代器(Iterator)
经过上一篇我们学过迭代器,大家对于迭代器接口的next方法应该不陌生,Generator函数也涉及到next()方法的调用,他们之间有什么关系呢?实现了迭代器接口的对象都可以for-of实现遍历。我们来测试下:
function* helloGenerator() {
yield "hello";
yield "generator";
return;
}
var h = helloGenerator();
for(var value of h){
console.log(value);//"hello","generator"
}
helloGenerarot对象是支持for-of循环的,也说明Generator函数在原型上实现了迭代器接口,上面调用的next()方法其实就是迭代器的next()方法。我们继续来看next()方法。
function* gen(x,y){
let z= yield x+y;
let result = yield z*x;
return result
}
var g = gen(5,6);
console.log(g.next());//{value: 11, done: false}
console.log(g.next());//{value: NaN, done: false}
分析上面的代码:
1、第一执行next(),运行"yield x+y",并返回x+y的运算结果11;
2、第二次执行next(),运行"yield z*x",此时是z为11,x为5,运算结果为55才对,为何是NaN呢?第一次运行到yield x+y,就将结果返回,实际没有执行z的赋值;第二次运行时,执行的是let z=undefined,所以运算z*x的结果是NaN。
那有没有办法解决这个问题,我们来改下这个例子:
function* gen(x,y){
let z= yield x+y;
let result = yield z*x;
return result
}
var g = gen(5,6);
console.log(g.next());//{value: 11, done: false}
console.log(g.next(11));//{value: 55, done: false}
请注意,我们第二次调用的时候,next方法中传入了参数11,此时得到正确的结果。next()方法是可以带参数的,其中的参数就替换了上一次yield执行的结果。在这个例子中z复制为11,即执行了let z=11,所以运行z*x时,返回了正确的值。
道友们可能要问,不能每次都算好上一次的运行结果,作为下一次next的入参吧,没人会在实际编码过程中这个干的,我们继续:
function* gen(x,y){
let z= yield x+y;
let result = yield z*x;
return result
}
var g = gen(5,6);
var i =g.next();//{value: 11, done: false}
g.next(i.value);//{value: 55, done: false}
这样就解决了,next()方法执行后,会返回yield后面表达式的运算结果,将上一次的运算结果,作为下一次的入参传入,实现了"无缝对接"。
这里我们用这个例子主要解释next的入参和返回值的用法,在实际工程中,我们可以用下面这种简单的方式(特别感谢恍惚丶旧时光同学的提醒),大家可以琢磨下。
function* gen(x,y){
let z,result;
yield z= x+y;
yield result = z*x;
return result
}
var g = gen(5,6);
g.next();//{value: 11, done: false}
g.next();//{value: 55, done: false}
对于迭代器(Iterator)接口,还有一个return()方法,我们来看下:
function* gen(x,y){
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next();//{value: 1, done: false}
g.next();//{value: 2, done: false}
g.return();//{value: undefined, done: true}
g.next();//{value: undefined, done: true}
执行return()方法后就返回done:true,Generator 函数遍历终止,后面的yield 3不会再执行了。与next()方法一样,return()也可以带参数。
function* gen(x,y){
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next();//{value: 1, done: false}
g.next();//{value: 2, done: false}
g.return(5);//{value: 5, done: true}
g.next();//{value: undefined, done: true}
此时,value就是return传入的值,执行return后结束,调用next(),将不会执行 yield 3。
三、yield 表达式
上面我们说到yield是Generator函数的暂缓执行的标识,对于yield只能配合Generator函数使用,在普通的函数中使用会报错。可以执行下面的代码,看下结果
function gen(x,y){
yield 1;
yield 2;
yield 3;
}//报错
Generator函数中还有一种yield*这个表达方式,看看它有什么作用。
function* foo(){
yield "a";
yield "b";
}
function* gen(x,y){
yield 1;
yield 2;
yield* foo();
yield 3;
}
var g = gen();
console.log(g.next());//{value: 1, done: false}
console.log(g.next());//{value: 2, done: false}
console.log(g.next());//{value: "a", done: true}
console.log(g.next());//{value: "b", done: true}
console.log(g.next());//{value: "3", done: true}
我们来分析下过程,当执行yield*时,实际是遍历后面的Generator函数,等价于下面的写法:
function* foo(){
yield "a";
yield "b";
}
function* gen(x,y){
yield 1;
yield 2;
for(var value of foo()){
yield value;
}
yield 3;
}
注意:yield* 后面只能适配Generator函数。
四、应用
讲了这么多,那么Generator函数用在什么场景呢?要回答这个问题,首先我们总结Generator它的特点,一句话:可以随心所欲的交出和恢复函数的执行权,yield交出执行权,next()恢复执行权。我们举几个应用场景的实例。
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提供的一种异步编程解决方案"。我认为它解决异步编程的两大问题
- 回调地狱
- 异步流控
回调地狱可以参见我的第一篇Promise,这里不做阐述。
那什么是异步的流控呢,简单说就是按顺序控制异步操作。下面我们就从工序的角度重新实现肚包鸡实例(上面的肚包鸡制作实例,是从角色的角度,这种模式其实并不是最佳的),每个工序都是可认为异步的过程,工序之间又是同步的控制(上一个工序完成后,才能继续下一个工序),这就是异步流控。
普通方法实现肚包鸡的制作过程:
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打印如下内容。
我们分析下执行过程:
1、每个工序对应一个独立的函数,在task中组合成工序列表,执行时将task作为入参传给run方法。run方法实现工序的流程控制。
2、首次执行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是ES6中非常重要的一个特性,本文仅介绍了其基本的知识和用法,大家有兴趣可以进一步深入了解。