# 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} 

# 别人的理解

链接 (opens new window)

# 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()恢复执行权。

  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);//开始执行
//prepare chicken
//fired chicken
//stewed chicken
//sdd ingredients
//serve chicken

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

  1. 异步编程

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}
最后更新: 11/23/2024, 1:16:30 PM