JavaScript中的生成器函数是如何工作的?

3

我看到这篇文章中的代码,它们展示了函数生成器的工作流程。

var foo, f;

foo = function* () {
  console.log('generator 1');
  console.log('yield 1', yield 'A');
  console.log('generator 2');
  console.log('yield 2', yield 'B');
  console.log('generator 3');
};

f = foo();

console.log('tick 1');
console.log(f.next('a'));
console.log('tick 2');
console.log(f.next('b'));
console.log('tick 3');
console.log(f.next('c'));
console.log('tick 4');
console.log(f.next('d'));

这是终端中的日志:

tick 1
generator 1
{ value: 'A', done: false }
tick 2
yield 1 b
generator 2
{ value: 'B', done: false }
tick 3
yield 2 c
generator 3
{ value: undefined, done: true }
tick 4
{ value: undefined, done: true }

但我找不到一种易于理解这个过程的方法,感觉很奇怪。如果有人有一个简单的方法来解决这个问题,请帮忙解释一下。


你特别觉得哪里很奇怪? - Bergi
学习了如何生成抽象语法树后,你会发现代码的顺序完全正确且非常清晰。 - rishat
2个回答

6

基础知识

调用生成器函数会返回一个迭代器

在迭代器上调用.next()会返回一个对象,格式如下:

{ 
  value // current value of the iterator, 
  done  // boolean indicating if iteration is finished 
}

在生成器提供的迭代器上调用.next()方法会从生成器中当前暂停的点到下一个yield运行代码,将生成器在这个下一个yield处暂停,并将任何yield值作为对象形式从迭代器的.next()方法中返回的值推出。

您传递给此迭代器的任何内容都将从生成器中当前暂停的yield返回。

由于在第一次调用.next()时生成器没有在yield上暂停,因此传递给第一个.next()的任何内容都将被忽略。

如果没有剩余的yield语句,则函数返回的任何内容都将是最后的迭代器值。

此时,done标志将设置为true,任何进一步调用.next()都将返回相同的值。


运行示例

因此,在您的代码方面,以下是正在发生的情况。我将注释每个执行步骤之后发生的每一行。

步骤1

f = foo();

此时,迭代器被创建并存储在变量f中,但是生成器中的任何代码都没有实际运行。因此我们有:

function* () {
  console.log('generator 1');
  console.log('yield 1', yield 'A');
  console.log('generator 2');
  console.log('yield 2', yield 'B');
  console.log('generator 3');
};

步骤2

f.next('a'); // returns { value: 'A', done: false }

这将运行生成器中第一个yield之前的代码,并将产生的'A'.next()调用中推出。传递到.next()'a'只是被忽略了,因为这是第一次调用(如上所述)。

注释掉运行的行,我们得到:

function* () {
  // console.log('generator 1');
  console.log('yield 1', PAUSE_POINT); // we're paused on the `yield`. Essentially half of this line is done
  console.log('generator 2');
  console.log('yield 2', yield 'B');
  console.log('generator 3');
};

第三步

f.next('b'); // return { value: 'B', done: false }

这将从第一个yield(第一个PAUSE_POINT)返回'b',并运行代码到下一个yield,从迭代器中推出'B'

删除运行的代码后,剩下:

function* () {
  // console.log('generator 1');
  // console.log('yield 1', 'b'); // this PAUSE_POINT returns 'b'
  // console.log('generator 2');
  console.log('yield 2', PAUSE_POINT); // no we're paused here
  console.log('generator 3');
};

第四步

f.next('c'); // { value: undefined, done: true }

当生成器暂停时,它会传递出'c',由于没有更多的yields,它将运行到生成器的结尾并推出生成器返回的任何内容,而在您的情况下,这只是隐式的return undefined。由于我们到达了生成器函数的结尾,done标志设置为true

function* () {
  // console.log('generator 1');
  // console.log('yield 1', 'b'); // this PAUSE_POINT returns 'b'
  // console.log('generator 2');
  // console.log('yield 2', 'c'); // this PAUSE_POINT returns 'c'
  // console.log('generator 3');
  // here we have an implicit return undefined;
};

第五步及以后

f.next('d'); // { value: undefined, done: true }

当生成器完成(即donetrue)后,对.next()的任何调用都只会返回最后一个值。将'd'传递到此方法中已经没有意义了。


附加示例

var foo = function* () {
  console.log('first call to next runs to yield #1');
  var a = yield 'A'; // this yield pushes 'A' and returns 'a'
  console.log('second call to next runs to yield #2');
  var b = yield 'B'; // this yield pushes 'B' and returns 'b'
  console.log('third call to next runs to the end of the generator ');
  // there's no return statement here so we are returning undefined
};

var f = foo();

console.log(f.next('this gets ignored')); // { value: 'A', done: false }
console.log(f.next('a'));                 // { value: 'B', done: false }
console.log(f.next('b'));                 // { value: undefined, done: true }

// any further call just returns whatever the last returned value was
console.log(f.next('this also gets ignored since we are done')); // { value: undefined, done: true }


有点奇怪的是,为什么这行代码 console.log('yield 1', yield 'A'); 没有完全执行,只有 yield 'A' 被执行并将 'A' 传递给结果对象,而 'yield 1' 字符串则在下一次执行。 - thelonglqd
1
因为这就是生成器的定义方式。它是一个可暂停的函数,在每个yield处暂停,不管它是否在一行中间或单独一行。当你调用.next()时,你会到达下一个yield并暂停。如果你再次调用next,你将从暂停的地方开始,并重复相同的过程。仔细看看我的答案中的步骤3,其中PAUSE_POINT变成了'b' - nem035
我明白了!谢谢你详细的解释。太好了! - thelonglqd
@Karim 很高兴能帮忙 :D - nem035

4
简单来说,yield关键字会停止执行代码,yield就像断点,下一个生成器调用会执行上一个断点后的代码直到下一个断点(如果有的话)。因此,生成器函数被分成几个部分,在我的示例代码中可以查看注释。

var gen=function*(){
  
  //1.start of first call next()
  
  yield "A";//1.here first next() ends
  
  //2.start of second call next();
  
  yield "B";//2.here end second next();
  
  //3.start of third call next()
  
  
  yield "C";//3.end of third next() call
  
  //NEXT AFTER THIRD. here every next() after third starts
  
  //no code no yield so value is undefined
  
  //here every next() after third ends
  
  
}();

console.log(gen.next().value);//A
console.log(gen.next().value);//B
console.log(gen.next().value);//C
console.log(gen.next().value);//undefined


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接