谈谈 ES6 中的 Generator 生成器

前言

ES6新特性 Generator,翻译过来就是生成器的意思,也不知道为什么官方会用这个名字,因为光从它的意思来看,感觉跟他的作用毫无关联。但既然都已经这么叫了,那我们也就从了吧。

Generator 是什么?

ECMAScript 6 中提供的一个新特性。回想一下,我们之前的函数可以有那些行为?我想到的就只有一种,收到命令马上执行并且是一口气执行完,除非程序中途出错。而现在 ECMAScript 6 时代给函数赋予了一个“特异功能”,它可以让函数在指定的地方暂停,然后等待命令再次执行剩余代码,更让人激动的是它可以在函数中设置多个甚至无数个(只要你乐意)这样的“关卡”。

Generator 写法

首选生成器的写法有四种:

// 写法1
let generator = function* () { ...... }
// 写法2
let generator = function *() { ...... }
// 写法3
function* generator() { ...... }
// 写法4
function *generator() { ...... }

至于用哪一种这个决定权在你手里。

不管怎样本文统一使用第一种形式进行书写,下面来看一个简单的例子:

const generator = function* () {
  console.log('代码块0');
  yield 1
  console.log('代码块1');
  yield 2;
  console.log('代码块2');
  yield 3;
  console.log('代码块3');
}
const gen = generator() // 创建了一个 generator 对象,但此时还没有去执行它里面的代码。
const g1 = gen.next() // 第一次调用 next() 开始执行函数,当遇到 yield 时中断执行,并返回 {value: 1, done: false} ,value 的值就是遇到的第一个 yield 后面的值。
const g2 = gen.next() // 第二次调用 next() 继续执行函数,当遇到 yield 时又中断执行,并返回 {value: 2, done: false} ,value 的值就是遇到的第二个 yield 后面的值。
const g3 = gen.next() // 第三次调用 next() 继续执行函数,当遇到 yield 时又中断执行,并返回 {value: 3, done: false} ,value 的值就是遇到的第三个 yield 后面的值。
const g4 = gen.next() // 第四次调用 next() 继续执行函数,此时后面已经没有 yield 了,所以直接执行完后面的 console.log(3); 就完成了任务,并返回 {value: undefined, done: true}

对于生成器的执行过程,可以看代码里备注。不难看到一个 next() 对应一个对象,这个对象有两个属性 value,和 done,value 属性的值为对应 yield 后面的值。即一一对应。

next 方法传数

如果我们向 next() 方法中传入参数,那么这个参数值就会作为上一个 yield 的返回值。

这里有两点需要注意:

1、如果不通过外部的 next() 方法传参的方式来修改 yield 的返回值,那么 yield 的返回值永远是 undefined。

2、由于 next() 方法传入的参数是作为上一个 yield 的返回值,所以第一次调用 next 的时候我们是不需要也不应该给 next() 方法传参,因为即使传了不会有任何效果。

下面是一个简单的例子:

const generator = function* () {
  let a = 0
  a = yield 1
  console.log('a 的值为:', a)
}
const gen = generator()
const g1 = gen.next()    
const g2 = gen.next(8)
// 结果:
// g1 => {value: 1, done: false}
// a 的值为: 8
// g2 => {value: undefined, done: true}

函数有 return

return 在函数在极常见,所以当 return 出现在了生成器函数中会又会怎么样呢?

不管怎么样,有一点我们可以确定,return 后面的代码永远不会被执行(终结函数执行)。

const generator = function* () {
  yield 'first'
  return 'return'
  yield 'end'
}
const gen = generator()
const g1 = gen.next()
const g2 = gen.next()
const g3 = gen.next()
// 结果:
// g1 => {value: "first", done: false}
// g2 => {value: "return", done: true}
// g3 => {value: undefined, done: true}

从上面这个例子可以证明 return 仍然是代码终结者,遇到 return ,其后面不管有没有 yield 都会被忽略,并且返回对象中的 done 值为 true。不管生成器函数中只有一行 return 语句还是多行语句中出现了 return ,结果都是一样的。

生成器函数调用生成器函数

如果想在生成器函数中调用另一个生成器函数,些时写法跟在一般的函数中调用函数有所区别。

const generatorInner = function* (){
  yield 2
  yield 3
}
const generator = function* () {
  yield 1
  yield* generatorInner()
  yield 4
}
const gen = generator()
const g1 = gen.next()
const g2 = gen.next()
const g3 = gen.next()
const g4 = gen.next()
const g5 = gen.next()
// 结果:
// g1 => {value: 1, done: false}
// g2 => {value: 2, done: false}
// g3 => {value: 3, done: false}
// g4 => {value: 4, done: false}
// g5 => {value: undefined, done: true}

也就是说如果想在生成器函数中调用生成器函数,那么你就需要在函数前面添加 yield* 。其实上上面的代码相当于:

const generator = function* () {
  yield 1
  yield 2
  yield 3
  yield 4
}

前面说到了 return ,那如果生成器函数中有 return 呢,情况又是怎么样子?

第一种情况:return 在 generator 生成器函数中,此时就按前面说到的 return 效果一样(终结者)

第二种情况: return 在 generatorInner 生成器函数中,此时如果 return 在函数的未尾那么不会有什么作用,一切都会像没有 return 一样执行,而如果 return 在 yield 之间,那么 generatorInner 中 reurn 后面的代码就不会被执行到,或者说被跳过,下面是一个简单的例子:

const generatorInner = function* (){
  yield 2
  return 'return'
  yield 3
}
const generator = function* () {
  yield 1
  yield* generatorInner()
  yield 4
}
const gen = generator()
const g1 = gen.next()
const g2 = gen.next()
const g3 = gen.next()
const g4 = gen.next()
// 结果:
// g1 => {value: 1, done: false}
// g2 => {value: 2, done: false}
// g3 => {value: 4, done: false}
// g4 => {value: undefined, done: true}

其它的情况你自己可以调整上面代码的 return 的位置并运行代码查看结果。

for of 循环

由于 Generator 函数是一个遍历器生成函数, Generator 函数执行后,返回一个遍历器对象, 所以我们可以使用 for of 循环对它进行遍历。

const generator = function* () {
  yield 1
  yield 2
  yield 3
}
const gen = generator()
for (let value of gen) {
  console.log(value); // 1 2 3
}

当 for of 遇到 {value: undefined, done: true } 时就会退出遍历,并且 undefined 值不会被返回。如果代码中有 return 语句,当遍历到 return 时就会退出遍历。

自带的 return() 方法

return() 方法可以终止遍历,并返回 { value:xxx,done: true },返回对象中的 value 用 xxx 来替代,因为它有两种可能:

第一种,如果没有给 return() 方法传参数的话,那么它的值就是 undefined ,

第二种:如果在调用 return() 方法时传入了参数,那么参数值就作为 value 的值。

const generator = function* () {
  yield 1
  yield 2
  yield 3
}
const gen = generator()
const g1 = gen.next()
const g2 = gen.return(8)
const g3 = gen.next()
// 结果:
// g1 => {value: 1, done: false}
// g2 => {value: 8, done: true}
// g3 => {value: undefined, done: true}

由于 return() 方法传入了参数,所以 g2 的 value 值为 8。

应用场景(用同步的方式写异步代码)

下面我们来定义几个定时器函数,然后通过生成器函数来同步处理它们。

function st1(sucess){
  setTimeout(function(){
    console.log("setTimeout 1");
    sucess();
  },1500)
}

function st2(sucess){
  setTimeout(function(){
    console.log("setTimeout 2");
    sucess();
  },500)
}

function st3(sucess){
  setTimeout(function(){
    console.log("setTimeout 3");
    sucess();
  },100)
}

//生成器对象流程处理
function handler(fn){
  const gen = fn();
  function next() {
    const result = gen.next();
    if ( result.done ){ // 如果结束,则直接返回,不继续执行
      return
    }
    result.value(next);
  }
  next(); // 启用
};

function* asyncQueue(){
    yield st1;
    yield st2;
    yield st3;
}

// 开始执行,处理异步队列
handler(asyncQueue);
// 结果:
// setTimeout 1
// setTimeout 2
// setTimeout 3

上面通过 setTimeout 来实现简单的异步代码,然后在生成器函数中通过 yield 处理每一个异步方法。由于我们 yield 后面是函数,所以这里的 value 值就是一函数,我们在这里调用并传入自定义的 next() 方法作为参数让程序执行完后,再次执行 next() 方法,执行下一轮,直到遍历完所有的 yield。

基于上面的例子,下面我们配合 promise 做一个简单的示例:

function asyncHandler(fn) {
    // 获取生成器对象
    const gen = fn();
    // 返回一个 promise 实例
    return new Promise((resolve, reject) => {
      // type 传入 next 方法名,如果为 err,那么会被 try{}catch(){}捕获, arg 就是将要给 next() 方法传入的参数
      function next(type, arg) {
        let result, value
        try {
          // 拿到 next() 的返回值
          result = gen[type](arg);
          // 取出值(这但是一个 promise)
          value = result.value;
        } catch (error) {
          reject(error);
          return;
        }
        // 如果已经遍历完
        if (result.done) {
          resolve(value);
        } else {
          // 否则,通过 Promise.resolve(value).then() 来有序调用 step 方法实现递归,进入下一轮遍历
          // 这里再用一个 Promise.resolve(value) 是因为如果这个 value 不是我们 promise 时,就把它转化为 promise
          // 如果你确定这个value 一定是一个 promise,那么你也可以不使用 Promise.resolve(value) 来对值进行转化
          return Promise.resolve(value).then(value => next('next', value), err => next('err', err));
        }
      }
      // 开始执行
      return next('next');
    })
}

// 创建异步代码
function waitAMinute(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(()=>{
      resolve(ms)
    }, ms);
  });
}

// 异步函数
const asyncQueue = function* () {
  // 这里遍历出5个异步,并且第一个异步后面都跟着打印出结果值
  for (let i = 0; i < 5; i++) {
    const response = yield waitAMinute(i * 50);
    // 这里就可以获取到异步返回的值,然后再根据这个结果值执行一些代码,这时是不是有点像 async/await 的用法了。
    console.log(response)
  }
}

// 使用异步处理函数处理异步队列
asyncHandler(asyncQueue)
// 结果:
// 0
// 50
// 100
// 150
// 200

我们在使用时,只需要像 asyncQueue 那样处理写代码就可以。然后把 asyncQueue 传入 asyncHandler() 方法处理即可。