你不知道的生成器

1,045 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情

前言

你不知道的迭代器 - 掘金 (juejin.cn)

在上一篇文章中,我们详细的讨论了什么是 迭代器,这篇文章依然从迭代器出发,进而展开地介绍什么是 生成器,以及生成器的高级应用场景

1. 迭代器回顾

我们说一个对象实现了 Symbol.iterator 可迭代协议,那么这个调用这个对象的 Symbol.iterator 方法就可以返回一个独立的迭代器

const obj = {
  [Symbol.iterator]() {
    return {
      next() {
        // ...
        return { done: true, value: undefined };
      },
    };
  },
};

for (const i of obj) {
  console.log(i);
}

只要实现了 1迭代器 接口我们可以改造任意类型数据结构将其转换为可迭代对象

我们得花费大量的代码去构造一个可迭代对象,而在 ES6 中,除了实现对象中的 Symbol.iterator 这种方式,我们还可以使用 ES6 提供的生成器,简化代码

下面是一个简单的生成器的例子

function* generatorFn() {
  yield 1;
  yield 2;
  yield 3;
}

for (const i of generatorFn()) {
  console.log(i);
}
// 1
// 2
// 3

对于 function 关键词后面这种带 * 的函数我们就可以说它是生成器了,下面我们就对这个例子详细的展介绍什么是 生成器

2. 生成器也是迭代器?

我们知道,能被 for~of 循环的对象一定实现了 Symbol.iterator 接口,在上面的这个例子中,我们调用生成器函数,生成器函数返回值竟然也可以被 for~of 循环

2.1 生成器函数的返回值

const generator = generatorFn();

生成器函数返回值:

image.png

我们执行生成器函数,返回值会产生一个生成器对象,在这个对象的原型上我们看到了 Symbol.iterator 接口,所以这个对象能被 迭代

3. 生成器基本使用

生成器的形式就是一个工厂函数,在普通函数的 function 关键字的前面添加一个 *,就可以定义生成器了,调用生成器的工厂函数就会一个独立的 生成器对象

生成器对象:
image.png

生成器对象被构造出来的时候处于 suspended 暂停执行的状态,我们调用一次 next 方法,就会开始执行生成器,next 方法来源于 Generator,注意与 Symbol.iteratornext 区别

函数体没有 yield

function* generatorFn() { }

const generator = generatorFn();
generator.next(); // {value: undefined, done: true}

image.png

next 函数的返回值为 {value: any, done: boolean}; 当 donetrue 的时候,生成器对象的状态为 closed,表示整个生成器对象 声明周期 的结束。我们为什么提到了 声明周期 的关键字,下面的代码能够很好的说明

函数体有 yield

function* generatorFn() {
  yield 1;
  yield 2;
  yield 3;
}

const generator = generatorFn();
generator.next(); // {value: 1, done: false}
generator.next(); // {value: 1, done: false}
generator.next(); // {value: 1, done: false}
generator.next(); // {value: undefined, done: true}

在执行 4 次 next 方法后,生成器对象的状态为 closed,缺少任何一次 next,生成器对象的状态都会为 suspended
image.png

这说明生成器函数的 yield 关键字可以分割函数体内部的代码,并且 yield 可以作为 next 中的 value

3.1 生成器函数的 next 方法

next 方法对于生成器函数有很重大的意义

function* generatorFn() {
  console.log(1);
}

const generator = generatorFn(); // 不输出任何代码

调用生成器工厂函数的时候,并不会执行生成器函数内部的代码,只有执行 next 的方法之后,整个工厂函数内部的代码才会执行,一直执行到遇到第一个 yield 之前,就像下面这个例子

function* generatorFn() {
  console.log(1);
  yield 1;
  console.log(2);
  yield 2;
}

const generator = generatorFn();
console.log(generator.next());

image.png

调用一次 next 方法,代码只会运行到一个 yield 之前停止位置

我们可以认为 next 方法和 yield 关键字控制整个生成器工厂函数内部的代码运行暂停的过程

这也是生成器最重大的意义

3.2 生成器函数的 return 值

return 的行为和 yield 十分类似

function* generatorFn() {
  return 1;
}

const generator = generatorFn();
generator.next(); // {value: 1, done: true}
generator; // closed 状态

它们的不同之处:通过 yield 关键字退出的生成器函数会处在 done: false 状态;通过 return 关键字退出的生成器函数会处于 done: true 状态。

对于没有明确 return 值的函数,默认 return undefined,所以在执行了超出 yield 关键字个数 next 方法之后,也会让生成器对象的状态变为 closed

3.3 yield 输入与输出

输入:

function* generatorFn(initVal: number) {
  console.log(initVal);
  console.log((yield) as number);
  console.log((yield) as number);
}

const generator = generatorFn(1); // 传给 initVal
generator.next(2); // 不会输出
generator.next(3); // 3
generator.next(4); // 4

生成器工厂函数next 都可以传参,第一次调用 next传入的值不会被使用,因为这一次调用是为了开始执行生成器函数

输出就比较好理解了:
下面这个例子利用生成器模拟 Array.prototype.fill

function initArray(n: number, x: any) {
  return Array.from(
    (function* (n: number, x: any) {
      while (n--) {
        yield x;
      }
    })(n, x)
  );
}

initArray(5, 0); // [0, 0, 0, 0, 0]

3.4 yield*

function* generatorFn() {
  yield 1;
  yield 2;
  yield 3;
}

对于 yield 输出的情况,如果想同时大批量输出,可以使用 yield*,该操作符接收一个可迭代对象

function* generatorFn() {
  yield* [1, 2, 3];
}

与下面的代码等效

function* generatorFn() {
  for (const i of [1, 2, 3]) {
    yield i;
  }
}

使用生成器可以很好地简化迭代器

const obj = {
  value: '123',
  *[Symbol.iterator]() {
    yield* this.value;
  },
};

for (const i of obj) {
  console.log(i);
}
// 1
// 2
// 3

4. 停止生成器

生成器对象除了多次执行 next 方法,消耗尽 yield 之外停止生成器,还有 returnthrow 方法可以更加主动的停止生成器,这两个方法在任意的生成器对象中都有,不同于自定义迭代器需要手动添加

4.1 return

return 方法会强制生成器进入关闭状态

function* generatorFn(): Generator<number, void | string, undefined> {
  yield* [1, 2, 3];
}

const g = generatorFn();

console.log(g.next());
console.log(g.return('结束'));
console.log(g.next());

image.png

生成器只要进入 closed 状态,就无法恢复了

4.2 throw

throw 方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭

function* generatorFn(): Generator<number, void | string, undefined> {
  yield* [1, 2, 3];
}

const g = generatorFn();
try {
  g.throw('错误');
} catch (e) {
  console.log(e); // 错误
}

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

外部处理错误,生成器对象状态依然会被关闭

如果在内部处理错误,生成器就不会被关闭:

function* generatorFn(): Generator<number, void | string, undefined> {
  for (const i of [1, 2, 3]) {
    try {
      yield i;
    } catch (e) {
      console.log(e);
    }
  }
}

const g = generatorFn();
g.next();
g.throw('结束');

console.log(g.next()); // {value: 3, done: false}

5. 生成器与 Promise 控制异步流程

我们我知道了 yield 可以配合 next 方法一起控制整个生成器函数的暂停与恢复执行的流程,所以,生成器也可以控制异步流程

7张图,20分钟就能搞定的async/await原理!为什么要拖那么久? - 掘金 (juejin.cn) ,这篇文章详细讲解了 async ~ await 语法糖

这里奉上 ts 的写法:

function fn(nums: number): Promise<number> {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(nums * 2);
    }, 1000);
  });
}
function* gen(): Generator<any, Promise<number>, any> {
  const num1 = yield fn(1);
  const num2 = yield fn(num1);
  const num3 = yield fn(num2);
  return num3;
}

// async 语法糖
function generatorToAsync(
  generatorFn: (...args: any[]) => Generator<any, Promise<any> | string, any>
) {
  return function (this: any, ...args: any[]) {
    const g = generatorFn.apply(this, args);

    return new Promise((resolve, reject) => {
      function go(key: 'throw' | 'next', arg?: any): Promise<any> | void {
        let res: IteratorResult<any, string | Promise<any>>;
        try {
          res = g[key](arg);
        } catch (err) {
          return reject(err);
        }

        const { value, done } = res;
        if (done) {
          return resolve(value);
        } else {
          return Promise.resolve(value).then(
            (val: any) => go('next', val),
            (err: any) => go('throw', err)
          );
        }
      }

      go('next');
    });
  };
}

// 使用
// generatorToAsync 包装普通函数就可以变成 async 函数
const asyncFn = generatorToAsync(gen);

const res = asyncFn();
res.then(value => {
  console.log(value);
});

总结

  1. 生成器是一种特殊的函数。在关键字function后加上一个 * 就可以将这个函数声明为生成器函数。
  2. 调用生成器函数,返回一个内置了 Symbol.iterator 可迭代协议的生成器对象
  3. 我们可以使用 yieldnext 控制生成器函数的执行过程