Generator 函数

普通函数只可以返回一个值,或者不返回任何值(实际返回 undefined)

Generator 函数可以返回(‘yield’)多个值, 它们可以与 iterator 配合使用

要创建 Generator 函数,需要用一个特殊的语法function *

  1. function* generateSequence() {
  2. yield 1;
  3. yield 2;
  4. return 3;
  5. }

当调用generator函数时,并不会运行其代码,而是返回一个[object Generator]的特殊对象,我们称其为 “generator object”

  1. function* generateSequence() {
  2. yield 1;
  3. yield 2;
  4. return 3;
  5. }
  6. const a = generateSequence();
  7. console.log(a); //[object Generator]

到目前为止,上面的代码还没有运行,我们拿到的是generator对象。

当前代码的位置

generator 对象的主要方法是 next(),当调用该方法时,它就会开始运行,执行到最近的 yield语句,然后函数暂停,将yield的值返回出去。

next()的结果会返回一个对象,该对象有两个属性

  • value:yield 后的值
  • done:如果 generator 执行完成,则返回 true,如果没有则返回false
  1. function* generateSequence() {
  2. yield 1;
  3. yield 2;
  4. return 3;
  5. }
  6. const a = generateSequence();
  7. console.log(a);
  8. console.log(a.next()); // {value:1,done:false}

截止目前,我们获取到了第一个值,现在代码处于这里

image-20211018145002687

当我们再次调用 next方法,代码就会从当前的位置继续往下 yield,直到 donetrue为止

  1. console.log(a.next()); // {value:2,done:false}
  2. console.log(a.next()); // {value:3,done:true}

现在代码已经执行完成了,done的状态为true,如果继续往下执行,会返回 valueundefined的对象

  1. console.log(a.next()); // {value:undefined,done:true}

Generator 可迭代

每个generator 对象都有 next方法,这意味着它是可迭代的(iterable),他内置了iterable接口。

  1. function* generateSequence() {
  2. yield 1;
  3. yield 2;
  4. return 3;
  5. }
  6. const a = generateSequence();
  7. for (let item of a) {
  8. console.log(item);
  9. }
  10. // 1
  11. // 2

使用 for 循环时,会默认调用 next()方法,然后打印出value

….上面的例子中,并没有返回 3,这是因为当donetrue时,for..of循环会忽略 value

如果我们希望能够获得3 ,则必须使用 yield返回

  1. function* generateSequence() {
  2. yield 1; //{value:1,done:false}
  3. yield 2; //{value:2,done:false}
  4. yield 3; //{value:3,done:false}
  5. }
  6. const a = generateSequence();
  7. for (let item of a) {
  8. console.log(item);
  9. }
  10. // 1
  11. // 2
  12. // 3

由于 generator对象部署了iterable接口,所以我们可以使用展开符去展开它

  1. function* generateSequence() {
  2. yield 1; //{value:1,done:false}
  3. yield 2; //{value:2,done:false}
  4. yield 3; //{value:3,done:false}
  5. }
  6. const a = generateSequence();
  7. const arr = [...a];
  8. console.log(arr); // [1, 2, 3]

使用 Generator 迭代

下面我们为对象手写一个 iterable 接口

  1. let range = {
  2. from: 1,
  3. to: 5,
  4. // for..of range 在一开始就调用一次这个方法
  5. [Symbol.iterator]() {
  6. // ...它返回 iterator object:
  7. // 后续的操作中,for..of 将只针对这个对象,并使用 next() 向它请求下一个值
  8. return {
  9. current: this.from,
  10. last: this.to,
  11. // for..of 循环在每次迭代时都会调用 next()
  12. next() {
  13. // 它应该以对象 {done:.., value :...} 的形式返回值
  14. if (this.current <= this.L) {
  15. return {
  16. value: this.current++,
  17. done: false,
  18. };
  19. }
  20. return {
  21. done: true,
  22. };
  23. },
  24. };
  25. },
  26. };
  27. for (let item of range) {
  28. console.log(item); // 1 2 3 4 5
  29. }

上面代码主要做了如下工作

  • 给对象设置[Symbol.iterator]属性,这个属性是一个函数
  • 该函数返回一个iterator object,拥有 next方法
  • next方法返回包含 valuedone两个属性的对象

由于 Generator函数的语法糖可以帮助返回包含 valuedone两个属性的对象,所以我们可以对其进行改写

  1. let range = {
  2. from: 1,
  3. to: 5,
  4. *[Symbol.iterator]() {
  5. for (let i = this.from; i <= this.to; i++) {
  6. yield i;
  7. }
  8. },
  9. };
  10. const generator = range[Symbol.iterator]();
  11. console.log(generator.next()); // 1
  12. console.log(generator.next()); // 2
  13. console.log(generator.next()); // 3
  14. console.log(generator.next()); // 4
  15. console.log(generator.next()); // 5

for..of循环时,会默认调用[Symbol.iterator]next方法,然后取其返回对象的 value值,直到 donetrue为止

Generator 函数能够帮助生成 iterator接口所需要的内容:

  • 一个拥有 next方法的对象(此时为generator 对象)
  • 该对象next方法可以返回{done:boolean,value:any}结构的对象

所以Generator可以看作 iterator接口的语法糖

Generator 组合

使用 yield *指令可以将执行委托给另一个 generator,比如下面我们使用这个语法来将一个 generator组合到另一个 generator

  1. function* generateSequence(start, end) {
  2. for (let i = start; i <= end; i++) yield i;
  3. }
  4. function* generateAll() {
  5. yield* generateSequence(1, 6); // *
  6. for (let i = start; i <= end; i++) yield i; // *行与这行代码效果是一样的
  7. }
  8. const g = generateAll();
  9. for (let item of g) {
  10. console.log(item); // 1 2 3 4 5 6 7 8 9 10
  11. }

Generator 传递参数

yield不仅可以向外返回结果,还可以将外部的值传递到 generator内部。

调用 generator.next(arg),我们就可以将参数arg传递到 generator 内部,这个 arg 参数会变成yield的结果。

  1. function* gen() {
  2. // 向外部代码传递一个问题并等待答案
  3. let result = yield '2 + 2 = ?'; // (*)
  4. alert(result);
  5. }
  6. let generator = gen();
  7. console.log(generator.next()); // 结果为 {done: false,value: "2 + 2 = ?"}
  8. console.log(generator.next(4)); // 会弹出 4 但结果为 {done: true,value: undefined}

yield 会将外部传递的参数 4 传递给 result,然后通过弹框打印出来。此时 yield 已执行结束,所以返回给外部的 yield 值为{done: true,value: undefined}

image-20211018173631723

  • 第一次调用generator.next()时,会经过 yield,此时往外部返回 yield值。(如果此时传递 arg 参数,那么会被忽略)它往外部返回第一个yield结果 {done: false,value: "2 + 2 = ?"}
  • 此时,在第*行,generator 执行暂停
  • 当调用generator.next(4)时,generator 恢复,并获取外部传入的 4 作为result:let result = 4

我们可以发现,generator 是通过yield来向外部传递值,外部则通过调用 next方法向 generator函数内部传递参数来交换结果。

根据上面的结论,我们再来看一个例子

  1. function* gen() {
  2. let ask1 = yield '2 + 2 = ?';
  3. alert(ask1); // 4
  4. let ask2 = yield '3 * 3 = ?';
  5. alert(ask2); // 9
  6. }
  7. let generator = gen();
  8. console.log('yield', generator.next()); //yield {done: false,value: "2 + 2 = ?"}
  9. console.log('yield', generator.next(4)); //alert 4 yield {done: false,value: "3 * 3 = ?"}
  10. console.log('yield', generator.next(9)); //alert 9 yield {done: true,value: undefined}

我们可以发现:

  • 一共有两个 yield 关键字,那么如果希望传递给外部的 yieldvalue,则需要调用两次 next方法
  • 当调用第二次 next并传递arg时,会把arg当成结果传递给 ask1。然后发现后面还有 yield,就又往外传递。此时就相当于内外部互相传递了信息
  • 当调用第三次 next时,由于没有yield关键字了,所以就只是向内部传递一下arg,往外透出的是{done: true,value: undefined}

执行图:

image-20211018180128813

  1. 第一个 .next() 启动了 generator 的执行……执行到达第一个 yield
  2. 结果被返回到外部代码中。
  3. 第二个 .next(4)4 作为第一个 yield 的结果传递回 generator 并恢复 generator 的执行。
  4. ……执行到达第二个 yield,它变成了 generator.next(4) 调用的结果。
  5. 第三个 next(9)9 作为第二个 yield 的结果传入 generator 并恢复 generator 的执行,执行现在到达了函数的最底部,所以返回 done: true

这个过程就像“乒乓球”游戏。每个 next(value)(除了第一个)传递一个值到 generator 中,该值变成了当前 yield 的结果,然后获取下一个 yield 的结果。

generator.throw

外部代码可以通过generator.next方法将值传递给 generator,作为yield的结果,也可以在yield的代码行抛出一个 error,

如果想这样做,我们需要调用generator.throw来将 error抛到对应的yield所在行

  1. function* gen() {
  2. try {
  3. let result = yield '2 + 2 = ?'; // (1)
  4. alert('执行没有到这里,因为上面抛出了异常');
  5. } catch (e) {
  6. alert(e); // 显示这个 error
  7. }
  8. }
  9. let generator = gen();
  10. generator.next();
  11. generator.throw(new Error('这是一个错误')); // (2)
  • 当第一次调用generator.next()时,会在第(1)行yield一个结果出来,然后暂停
  • 外部调用generator.throw将错误抛给内部时,此时generator继续执行,执行到 let result,并被try..catch捕获到,抛给外部一个错误,后面的代码不会被继续执行

如果我们没有在generator内部捕获它,那么这个错误就会从 generator掉出到外部代码中。

  1. function* gen() {
  2. let result = yield '2 + 2 = ?'; // (1)
  3. alert('执行没有到这里,因为上面抛出了异常');
  4. }
  5. let generator = gen();
  6. try {
  7. generator.next();
  8. generator.throw(new Error('这是一个错误')); // (2)
  9. } catch (error) {
  10. alert(error);
  11. }

下面的代码效果是一样的。

小结

  • Generator 函数通过 function*关键字创建
  • 在 generator 内部,使用 yield关键字
  • 外部和generator内部可以通过next/yield调换结果

异步迭代

迭代器是采用 iterator接口的,如果希望给一个未内置迭代接口的对象设置iterator 接口,我们需要完成以下步骤:

  1. 给对象设置[Symbol.iterator]属性,这个属性指向一个 function
  2. function需要返回一个具有 next方法的对象,我们称之为iterator object
  3. 该对象的 next方法返回具有 done:booleanvalue:any属性的对象

以下是例子

  1. let range = {
  2. from: 1,
  3. to: 5,
  4. //for..of 循环开始时,会调用这个方法,获取 iterator object
  5. [Symbol.iterator]() {
  6. return {
  7. current: this.from,
  8. last: this.to,
  9. // for..of 每次循环,都会调用 next 方法,直到 done 为 true 为止
  10. next() {
  11. return this.current <= this.last
  12. ? // next 方法返回的对象必须为{done:boolean,value:any}的格式
  13. { done: false, value: this.current++ }
  14. : { done: true };
  15. },
  16. };
  17. },
  18. };
  19. for (let item of range) console.log(item); // 1 2 3 4 5

如果我们希望能够采用异步迭代,比如说 setTimeout 的延迟之后迭代,就需要用到异步迭代。

要让上面的对象拥有异步迭代的能力,我们需要做以下调整

  • [Symbol.iterator]修改为[Symbol.asyncIterator]
  • next方法返回一个 Promise,并且状态为 fulfilled
  • 使用 for await ..进行循环

下面是修改后的代码

  1. let range = {
  2. from: 1,
  3. to: 5,
  4. [Symbol.asyncIterator]() {
  5. return {
  6. current: this.from,
  7. last: this.to,
  8. // 这里的 async 是让return 的值变成 promise
  9. async next() {
  10. // 这里使用 await 阻塞,模拟延迟效果
  11. await new Promise(resolve => setTimeout(resolve, 1000));
  12. return this.current <= this.last
  13. ? { done: false, value: this.current++ }
  14. : { done: true };
  15. },
  16. };
  17. },
  18. };
  19. // 配合 for await 必须用 async
  20. (async () => {
  21. for await (let item of range) {
  22. console.log(item);
  23. }
  24. })();
  • next 方法可以不是async,这里只是为了让 next必须返回promise而写的语法糖
  • 为了让异步迭代生效,其内部必须具有Symbol.asyncIterator方法
  • 使用 for await 迭代,需要用上 async 关键字

以下是两者对比差异的表格:

Iterator 异步 iterator
提供 iterator 的对象方法 Symbol.iterator Symbol.asyncIterator
next() 返回的值是 任意值 Promise
要进行循环,使用 for..of for await..of

异步 generator

generator 可以很方便地帮助我们生成 iterator 所需要的东西:

——通过*关键字生成generator对象,内部包含 next方法

——通过 yield抛出 具有donevalue属性的对象

上面的代码可以修改成这样

  1. let range = {
  2. from: 1,
  3. to: 5,
  4. async *[Symbol.asyncIterator]() {
  5. for (let i = this.from; i <= this.to; i++) {
  6. await new Promise(resolve => setTimeout(resolve, 1000));
  7. yield i;
  8. }
  9. },
  10. };
  11. // 配合 for await 必须用 async
  12. (async () => {
  13. for await (let item of range) {
  14. console.log(item); // 1 2 3 4 5
  15. }
  16. })();

此时,generator.next()返回一个 promise,且它是一个异步的函数

  1. (async () => {
  2. const asyncGenerator = range[Symbol.asyncIterator]();
  3. const result = await asyncGenerator.next(); // 返回Promise Object {done:false,value:1}
  4. console.log(result); // {done:false,value:1}
  5. })();