# generator
在 iterable 对象中,包含 return()方法,只有当迭代器终止遍历时,才会触发此方法。例如,在 for-of 循环体中执行 break、continue 或 return 语句,甚至还能通过调用 throw 语句抛出自定义异常,提前结束代码的运行,以此实现触发条件,如下所示。
for (var value of iterable) {
throw new Error();
}
Generator通过yield标识位和next()方法调用,实现函数的分段执行。
function与函数名之间有一个星号 * ;函数体内部使用 yield 表达式,定义不同的内部状态。箭头函数不能用来定义生成器函数。
+ function *
+ yield && return
+ next()
+ yield*
调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停执行(suspended)的状态。与迭代器相似,生成器对象也实现了Iterator接口,因此具有next()方法。调用这个方法会让生成器开始或者恢复执行。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个 yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。 不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果, 而是一个指向内部状态的指针对象 。
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
function* generatorFn() {
for (const x of [1, 2, 3]) {
yield x;
}
}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.next()); // {value: 1, done: false}
console.log(g.return(4)); // {value: 4, done: true}
console.log(g); // generatorFn {<closed>}
生成器对象实现了Iterator接口,它默认的迭代器是自引用的。
function * gen(){}
console.log(gen) // ƒ * gen(){}
console.log(gen()) //gen {<suspended>}
console.log(gen()[Symbol.iterator])//ƒ [Symbol.iterator]() { [native code] }
let test = gen()
console.log(test === test[Symbol.iterator]())//true
# yield 表达式
yield后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为js提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
yield表达式与return语句既有相似之处,也有区别。
- 相似之处在于,都能返回紧跟在语句后面的那个表达式的值。
- 区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只执行一次return语句,但是可执行多个yield表达式。正常函数只能返回一个值。
- yield关键字还可以作为函数的中间参数使用,上一次让生成器函数暂停的yield关键字会接收到传给next()方法的第一个值。但是第一次调用next传入的值不会被用,因为这一次调用是为了开始执行生成器函数。
Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
a.forEach(function (item) {
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
});
};
for (var f of flat(arr)){
console.log(f);
}
//Uncaught SyntaxError: Unexpected identifier
上面代码也会产生句法错误,因为forEach方法的参数是一个普通函数,但是在里面使用了yield表达式。改用for循环。
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
var length = a.length;
for (var i = 0; i < length; i++) {
var item = a[i];
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
}
};
for (var f of flat(arr)) {
console.log(f);
}
// 1, 2, 3, 4, 5, 6
另外,yield表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
# yield *
使用星号增强yield行为,让它能够迭代一个可迭代的对象 ,从而一次产出一个值,其实这跟yield直接放在循环中时等价的;yield*表达式,也可以作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。
// 等价下面的方法
// function* generatorFn() {
// for (const x of [1, 2, 3]) {
// yield x;
// }
// }
function* generatorFn() {
yield* [1, 2 ,3];
}
let generatorObject = generatorFn();
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
function* generatorFn() {
yield* [1, 2];
yield *[3, 4];
yield * [5, 6];
}
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
// 4
// 5
// 6
function* generatorFn() {
yield* [1, 2];
yield *[3, 4];
yield [5, 6];
}
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
// 4
// [5, 6]
yield*的值是关联迭代器返回done:true时的value属性。对于普通迭代器来说,这个值就是undefined
function* generatorFn() {
console.log('iter value:', yield* [1, 2, 3]);
}
for (const x of generatorFn()) {
console.log('value:', x);
}
// value: 1
// value: 2
// value: 3
// iter value: undefined
# next 方法的参数
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
function* f() {
for(var i = 0; true; i++) {
var reset =30 + (yield i);
console.log(i+"++++")
console.log(reset+"----")
if(reset) { i = -1; }
}
}
var g = f();
console.log(g.next())
console.log(g.next(5))
console.log(g.next(-30))
//{value: 0, done: false}
//0++++
//35----
//{value: 0, done: false}
//0++++
//0----
//{value: 1, done: false}
function* foo(x) {
var y = 2 * (yield (x + 3));
var z = 1.5 * (yield (y / 3))
console.log(x,y,z)
return (x + y + z);
}
var a = foo(5);
// console.log(a.next())//{value: 8, done: false}
// console.log(a.next())//{value: NaN, done: false}
var b = foo(6);
console.log(b.next())
console.log(b.next(12))
let s=b.next(13)
console.log(s)
//{value: 9, done: false}
//{value: 8, done: false}
//6 24 19.5
//{value: 49.5, done: true}
上面代码中,第二次运行next方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),除以 3 以后还是NaN,因此返回对象的value属性也等于NaN。
如果向next方法提供参数,返回结果就完全不一样了。上面代码第一次调用b的next方法时,返回x+3的值9;第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8;第三次调用next方法,将上一次yield表达式的值设为13,13*1.5=19.5,这时x等于6,y等于24,所以return语句的值等于49.5。
注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。 从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
function* helloWorldGenerator() {
console.log('test')
console.log( yield 'hello','function1')
console.log( yield ,'function2')
return ;
}
var hw = helloWorldGenerator();
hw.next('3')
hw.next('4')
hw.next('5')
// test
// 4 function1
// 5 function2
var hw1 = helloWorldGenerator();
console.log(hw1.next('3'))
console.log(hw1.next('4'))
console.log(hw1.next('5'))
console.log(hw1.next('6'))
// test
// {value: 'hello', done: false}
// 4 function1
// {value: undefined, done: false}
// 5 function2
// {value: undefined, done: true}
// {value: undefined, done: true}
var hw2 = helloWorldGenerator();
let s1=hw2.next('3')
console.log(s1)
// test
// {value: 'hello', done: false}
# for...of 循环与generator
for...of循环可以 自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
上面代码使用for...of循环,依次显示 5 个yield表达式的值。这里需要注意, 一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。
直接显式调用next方法用处不大,如果把生成器对象当成可迭代对象 ,使用更加方便。
function* nTimes(n) {
if (n > 0) {
yield* nTimes(n - 1);
yield n - 1;
}
}
for (const x of nTimes(3)) {
console.log(x);
}
// 0
// 1
// 2
下面是一个利用 Generator 函数和for...of循环,实现斐波那契数列的例子。 链接
# 对象改造使用for...of
原生的 JavaScript 对象没有遍历接口,无法使用for...of循环,通过 Generator 函数为它加上这个接口,就可以用了。 (Reflect.ownKeys和Object.keys异同点)
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
上面代码中,对象jane原生不具备 Iterator 接口,无法用for...of遍历。这时,我们通过 Generator 函数objectEntries为它加上遍历器接口,就可以用for...of遍历了。加上遍历器接口的另一种写法是,将 Generator 函数加到对象的Symbol.iterator属性上面。
function* objectEntries() {
let propKeys = Object.keys(this);
for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
除了for...of循环以外,扩展运算符(...)、解构赋值和Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
// 扩展运算符
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解构赋值
let [x, y] = numbers();
x // 1
y // 2
// for...of 循环
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
# generator提前终止生成器
function* generatorFn() {
for (const x of [1, 2, 3]) {
yield x;
}
}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
try {
g.throw('foo');
} catch (e) {
console.log(e); // foo
}
console.log(g); // generatorFn {<closed>}
如果内部对错误进行了处理,那么这个生成器就不会关闭而且还可以恢复执行.
function* generatorFn() {
for (const x of [1, 2, 3]) {
try {
yield x;
} catch(e) {}
}
}
const g = generatorFn();
console.log(g.next()); // { done: false, value: 1}
g.throw('foo');
console.log(g.next()); // { done: false, value: 3}
# 别人的理解
# Generator 函数与迭代器(Iterator)
迭代器(Iterator),还有一个 return()方法
执行return()方法后就返回done:true,Generator 函数遍历终止,后面不会再执行,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}
# yield 表达式
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特点: 可以随心所欲的交出和恢复函数的执行权,yield交出执行权,next()恢复执行权。
- 协程
协程可以理解成多线程间的协作,比如说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);//开始执行
//prepare chicken
//fired chicken
//stewed chicken
//sdd ingredients
//serve chicken
按照大厨和伙计的角色,分别创建了两个Generator函数,chef和worker。函数中列出了各自角色要干的活,当要转交给其他人任务时,利用yield,暂停执行,并将执行权交出;run方法实现流程控制,根据yield返回的值,决定移交给哪个角色函数。相互配合,直到完成整个过程。
- 异步编程
Generator函数,官方给的定义是"Generator函数是ES6提供的一种异步编程解决方案"。它解决异步编程的两大问题
- 回调地狱
- 异步流控
异步的流控,简单说就是按顺序控制异步操作,以上面的肚包鸡为例,每个工序都是可认为异步的过程,工序之间又是同步的控制(上一个工序完成后,才能继续下一个工序),这就是异步流控。
我们用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() {
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);//开始执行
//prepare chicken
//fired chicken
//stewed chicken
//sdd ingredients
//serve chicken
我们来执行下这个过程,按照我们既定的工序顺序实现的。
我们分析下执行过程:
1.每个工序对应一个独立的函数,在task中组合成工序列表,执行时将task作为入参传给run方法。run方法实现工序的流程控制。
2.首次执行next()方法,gen.next()的value,即result.value返回的是prepare函数对象,执行result.value(next),即执行prepare(next);prepre执行完成后,继续调用其入参的next,即下一步工序,
3.以此类推,完成整个工序的实现。
从上面例子看,task方法将各类工序"扁平化",解决了层层嵌套的回调地狱;run方法,使各个工序同步执行,实现了异步流控。
# 实际应用
//ES6
function * draw (first = 1, second = 3, third = 5) {
let firstPrize = ['1A', '1B', '1C', '1D', '1E']
let secondPrize = ['2A', '2B', '2C', '2D', '2E', '2F', '2G', '2H', '2I']
let thirdPrize = ['3A', '3B', '3C', '3D', '3E', '3F', '3G', '3K', '3O', '3P']
let count = 0
let random
while (1) {
if (count < first) {
random = Math.floor(Math.random() * firstPrize.length)
yield firstPrize[random]
count++
firstPrize.splice(random, 1)
} else if (count < first + second) {
random = Math.floor(Math.random() * secondPrize.length)
yield secondPrize[random]
count++
secondPrize.splice(random, 1)
} else if (count < first + second + third) {
random = Math.floor(Math.random() * thirdPrize.length)
yield thirdPrize[random]
count++
thirdPrize.splice(random, 1)
} else {
return false
}
}
}
let d = draw()
console.log(d.next().value)
console.log(d.next().value)
console.log(d.next().value)
console.log(d.next().value)
console.log(d.next().value)
console.log(d.next().value)
console.log(d.next().value)
console.log(d.next().value)
console.log(d.next().value)
console.log(d.next().value)
# co库
co 函数库用于 Generator 函数的自动执行,避免手动next的问题。
npm install co
co(function* () {
var result = yield Promise.resolve(true);
return result;
}).then(function (value) {
console.log(value);
}, function (err) {
console.error(err.stack);
});
# 生成器的其他应用场景
function* f() {
for(var i = 0; true; i++) {
var reset =30 + (yield i);
console.log(i+"++++")
console.log(reset+"----")
if(reset) { i = -1; }
}
}
var g = f();
console.log(g.next())
console.log(g.next(5))
console.log(g.next(-30))
//{value: 0, done: false}
//0++++
//35----
//{value: 0, done: false}
//0++++
//0----
//{value: 1, done: false}
← iterator迭代器 三点运算符 →