jannahuang / blog

MIT License
0 stars 0 forks source link

生成器 Generator 是什么 #17

Open jannahuang opened 2 years ago

jannahuang commented 2 years ago

生成器 Generator

生成器(generator)函数能生成一组值的序列,但每个值的生成是基于每次请求。我们必须显式地向生成器请求一个新的值,随后生成器要么响应一个新生成的值,要么就告诉我们它之后都不会再生成新值。当对另一个值的请求到来后,生成器就会从上次离开的位置恢复执行。 普通函数只会返回一个单一值(或者不返回任何值)。而 generator 可以按需一个接一个地返回(“yield”)多个值。

generator 函数

创建一个生成器函数的语法:在关键字 function 后面加上一个星号(*)。生成器函数体内就能够使用新关键字 yield,从而生成独立的值。

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
} // 定义一个生成器,它能生成一个包含3个数据的序列

let generator = generateSequence(); // 调用生成器得到一个迭代器

调用生成器并不会执行生成器函数,而是会创建一个叫作迭代器(iterator) 的对象。 迭代器用于控制生成器的执行。迭代器对象暴露的最基本接口是 next() 方法,用来向生成器请求一个值,从而控制生成器。 当 next() 方法被调用时,它会恢复生成器的运行,执行直到最近的 yield 语句。然后函数执行暂停,并将产出的(yielded)值返回到外部代码。 next() 的结果始终是一个具有两个属性的对象

let one = generator.next(); console.log(JSON.stringify(one)); // {value: 1, done: false}

// 再次调用 generator.next(),代码恢复执行并返回下一个 yield 的值 let two = generator.next(); console.log(JSON.stringify(two)); // {value: 2, done: false}

// 第三次调用 generator.next(),代码将会执行到 return 语句 let three = generator.next(); console.log(JSON.stringify(three)); // {value: 3, done: true}, generator 执行完成

每当生成一个当前值后,生成器就会非阻塞地挂起执行,随后耐心等待下一次值请求的到达。
如果生成器中没有 return,则在最后一个 yield 调用之后,再次调用会返回 {value: undefined, done: true。
> function* f(…) 或 function *f(…) 两种写法都可以,通常更倾向于第一种语法,因为星号 * 表示它是一个 generator 函数,它描述的是函数种类而不是名称。

## 对迭代器进行迭代
通过调用生成器得到的迭代器,暴露出一个next方法能让我们向生成器请求一个新值。
试着用普通的while循环来迭代生成器生成的值序列:
```javascript
function* WeaponGenerator(){
  yield "Katana";
  yield "Wakizashi";
}
const weaponsIterator = WeaponGenerator();  // 新建一个迭代器
let item; // 创建一个变量,保存生成器产生的值
while(!(item = weaponsIterator.next()).done) {
  console.log(item !== null, item.value);
}
// 每次循环都会从生成器中取出一个值,然后输出该值。
// 当生成器不会再生成值的时候,停止迭代

这就是 for-of 循环的原理。for-of 循环不过是对迭代器进行迭代的语法糖。

for(var item of WeaponGenerator ()){
  console.log(item !== null, item);
}

generator 组合(把执行权交给下一个生成器)

正如在标准函数中调用另一个标准函数,我们可以把生成器的执行委托给另一个生成器。

function* WarriorGenerator(){
  yield "Sun Tzu";
  yield* NinjaGenerator(); // yield* 将执行权交给了另一个生成器
  yield "Genghis Khan";
}
function* NinjaGenerator(){
  yield "Hattori";
  yield "Yoshi";
}
for(let warrior of WarriorGenerator()){
  console.log(warrior !== null, warrior);
}
// 输出:Sun Tzu、Hattori、Yoshi、Genghis Khan

在迭代器上使用 yield* 操作符,程序会跳转到另外一个生成器上执行。 for-of 循环不会关心是否委托到另一个生成器上,它只关心在 done 状态到来之前都一直调用 next() 方法。

generator 双向通信

  1. 使用生成器函数参数传值
  2. 使用 next() 方法向生成传值
    
    function* NinjaGenerator(action) { // 生成器可以像其他函数一样接收标准 参数
    const imposter = yield ("Hattori " + action); // 生成器会返回一个中间计算结果。而在调用迭代器的 next() 方法传值时,可以将该值传递回生成器
    console.log(imposter === "Hanzo",
     "The generator has been infiltrated");
    yield ("Yoshi (" + imposter + ") " + action); // 传递回的值将成为yi eld表达式的返回值,因此impostrer的值是Hanzo
    }
    const ninjaIterator = NinjaGenerator("skulk");  // 普通的参数传递

const result1 = ninjaIterator.next(); console.log(result1.value === "Hattori skulk","Hattori is skulking"); // 触发生成器的执行,并检测返回值是否正确

const result2 = ninjaIterator.next("Hanzo"); // next() 方法传值时,imposter 结果即为该值 console.log(result2.value === "Yoshi (Hanzo) skulk", "We have an imposter!"); // 将数据作为next方法的参数传递给生成 器,并检测返回值是否符合预期

![双向通信](https://raw.githubusercontent.com/jannahuang/blog/main/pictures/generator-two-way.png)
另一个例子:
```javascript
function* gen() {
  let ask1 = yield "2 + 2 = ?";
  console.log(ask1); // 4
  let ask2 = yield "3 * 3 = ?"
  console.log(ask2); // 9
}

let generator = gen();

console.log( generator.next().value ); // "2 + 2 = ?"
console.log( generator.next(4).value ); // "3 * 3 = ?"
console.log( generator.next(9).done ); // true
  1. 第一个 .next() 启动了 generator 的执行……执行到达第一个 yield。
  2. 结果被返回到外部代码中。
  3. 第二个 .next(4) 将 4 作为第一个 yield 的结果传递回 generator 并恢复 generator 的执行。
  4. ……执行到达第二个 yield,它变成了 generator 调用的结果。
  5. 第三个 next(9) 将 9 作为第二个 yield 的结果传入 generator 并恢复 generator 的执行,执行现在到达了函数的最底部,所以返回 done: true。 每个 next(value)(除了第一个)传递一个值到 generator 中,该值变成了当前 yield 的结果,然后获取下一个 yield 的结果。

抛出异常

个迭代器除了有一个 next() 方法,还有一个 throw() 方法。

function* NinjaGenerator() {
  try{
    yield "Hattori";
    fail("The expected exception didn't occur"); // 此处的错误将不会发生
  } catch(e) {
    console.log(e === "Catch this!", "Aha! We caught an exception"); // 捕获异常并检测接收到的异常是否符合预期
  }
}
const ninjaIterator = NinjaGenerator();
const result1 = ninjaIterator.next();
assert(result1.value === "Hattori", "We got Hattori"); // 从生成器取一个值
ninjaIterator.throw("Catch this!");   // 向生成器抛出一个异常

generator 实际应用

用生成器生成ID序列

function* IdGenerator() {  // 定义生成器函数IdGenerator
  let id = 0;   // 一个始终记录ID的变量,这个变量无法在生成器外部改变 
  while (true) {
    yield ++id;
  }  // 循环生成无限长度的ID序列
}
const idIterator = IdGenerator(); // 这个迭代器我们能够向生成器请求新的 ID 值
const ninja1 = { id: idIterator.next().value };
const ninja2 = { id: idIterator.next().value };
const ninja3 = { id: idIterator.next().value };  //请求3个新ID值
assert(ninja1.id === 1, "First ninja has id 1");
assert(ninja2.id === 2, "Second ninja has id 2");
assert(ninja3.id === 3, "Third ninja has id 3"); // 测试运行结果

局部变量 id 仅能在该生成器中被访问,不必担心会不小心在代码的其他地方修改 id 值。随后是一个无限的 while 循环,其每次迭代都能生成一个新 id 值并挂起执行,直到下一次ID请求到达。

生成器内部构成

调用一个生成器不会实际执行它,而是创建了一个新的迭代器,通过该迭代器才能从生成器中请求值。在生成器生成了一个值后,生成器会挂起执行并等待下一个请求的到来。在某种方面来说,生成器的工作更像是一个在状态中运动的状态机。

以上笔记参考《现代 JavaScript 教程》,《JavaScript 忍者秘籍(第2版)》