banner
布语

布语

集中一点, 登峰造极⚡️ 布语、布羽、不语、卜语......
github
twitter

Generator的艺术

该主题分享参阅文章自阮一峰老师的 ECMAScript6 入门 => https://es6.ruanyifeng.com/#docs/generator#%E7%AE%80%E4%BB%8B

参阅列表:

  1. 阮一峰老师的《Generator 函数的含义与用法》: 点击参阅
  2. 稀土掘金平台作者前端罗纳尔多的《异步编程解决方案 generator》: 点击参阅

直观使用#

function* printStrGenerator() {
    let str1 = yield "Hello";
    console.log(str1);
    let str2 = yield str1 + "CodeGorgeous";
    console.log(str2);
    return "Done";
}

const print = printStrGenerator();
console.log(print); // printStrGenerator {<suspended>}
console.log(print.next()); // {done: false, value: "Hello"}
console.log(print.next("My is")); // {done: false, value: "CodeGorgeous"}
console.log(print.next("Test")); // {done: true, value: "Done"}
console.log(print.next()); // {done: true, value: undefined}
console.log(print); // printStrGenerator {<closed>}

Generator 产生的背景#

在 Js 中因为 JavaScript 为单线程操作,所以当我们想要发出一个请求时如果一味的等待请求响应再接着执行代码那么注定要浪费非常不稳定的时间,随之而来的就是为了解决这个问题推出一个方式叫做异步代码编程,大致可以理解为当代码执行到一个异步操作后会继续执行后续代码,等待异步操作完成后触发相应的操作然后接着执行同步代码,相应的提出这种思想后就推出了几种解决方案:

  1. 回调函数
  2. 事件监听
  3. 发布 / 订阅
  4. Promise

我们通过几个例子来重新看一下这几种解决方案及其优缺点

  • 回调函数

    • 示例:
    // 复制文件操作
    fs.readFile("./test/index.html", 'utf-8', (err, data) => {
        if (err) return;
        // 创建文件并复制写入内容
        fs.writeFile("./test/index-copy.html", data, 'utf-8', (err) => {
            if (err) return;
            console.log("复制成功");
        })
    });
    
    • 优点:
      1. 不会阻塞代码执行,易于书写
    • 缺点:
      1. 一个异步操作依赖前一个异步操作,易形成回调地狱 (callback hell)(高耦合)
      2. 代码不易维护
  • 事件监听

    • 示例:
      const fn = () => {
          fs.readFile("./test/index.html", 'utf-8', (err, data) => {
              if (err) return;
              fn.trigger("readFileOver", data);
          });
      };
      
      fn.eventList = [];
      
      fn.on = (type, callback) => {
          fn.eventList.push({
              type,
              fn: callback
          });
      };
      
      fn.trigger = (type, ...args) => {
          fn.eventList.forEach(item => {
              (item.type === type) && (item.fn(...args));
          });
      };
      
      fn.on("readFileOver", (data) => {
          fs.writeFile("./test/index-copy.html", data, 'utf-8', (err) => {
              if (err) return;
              console.log("复制成功");
          });
      });
      
      fn();
      
    • 优点:
      1. 相较于回调函数方式来讲基本不会形成回调地狱 (降低耦合)
      2. 可以自行绑定多个多个事件并可单独配置调整回调函数
  • 发布 / 订阅 (观察者模式)

    • 示例:
      class EventBus {
          constructor() {
              this.event = [];
          };
          /**
          * 订阅
          * @param {*} eventType 事件类型
          * @param {*} callback 回调函数
          */
          add(eventType, callback) {
              this.event.push({
                  type: eventType,
                  fn: callback
              });
          };
          /**
          * 发布
          * @param {*} eventType 事件类型
          * @param  {...any} args 参数
          */
          trigger(eventType, ...args) {
              // 此处就不对已触发事件进行清除操作
              this.event.forEach(item => {
                  if (item.type === eventType) {
                      item.fn(...args);
                  }
              });
          }
      };
      // 创建实例
      const eventBus = new EventBus();
      
      // 订阅readFileOver事件
      eventBus.add("readFileOver", (data) => {
          fs.writeFile("./test/index-copy1.html", data, 'utf-8', (err) => {
              if (err) return;
              console.log("复制成功1");
          });
      });
      eventBus.add("readFileOver", (data) => {
          fs.writeFile("./test/index-copy2.html", data, 'utf-8', (err) => {
              if (err) return;
              console.log("复制成功2");
          });
      });
      
      fs.readFile("./test/index.html", 'utf-8', (err, data) => {
          if (err) return;
          // 发布readFileOver事件
          eventBus.trigger("readFileOver", data);
      });
      
    • 优点:
      1. 相较于事件监听来讲发布 / 订阅模式更容易管理触发机制更加清晰了解到当前存在的事件情况
  • Promise

    • 示例:
      new Promise((resolve, reject) => {
          fs.readFile("./test/index.html", 'utf-8', (err, data) => {
              if (err) reject(err);
              resolve(data);
          });
      }).then(resp => {
          // 创建文件并复制写入内容
          fs.writeFile("./test/index-copy.html", resp, 'utf-8', (err) => {
              if (err) return;
              console.log("复制成功");
          });
      }).catch(err => {
          console.log("Error:", err);
      });
      
    • 优点:
      1. 重传统的回调函数内嵌模式改变为链式调用模式
      2. 更加清晰的代码流程
      3. 可读性提升

在传统的编程语言中早已有异步编程解决方案,其中还有一种方案叫做协程, 其大致意思为当个线程互相协作完成某个异步任务.
在 Js 中是以 Generator 函数的方式出现,以复制文件举例:

function* copyFile() {
    const data = yield fs.readFile("./test/index.html", 'utf-8', (err, data) => {
        if (err) return;
        else fn.next(data);
    });
    yield fs.writeFile("./test/index-copy.html", data, 'utf-8', (err) => {
        if (err) return;
        console.log("复制成功");
        fn.next();
    })
};
const fn = copyFile();
fn.next()

为什么 Generator 能够解决异步问题
Generator 最重要的一个特点就是可以交出函数的执行权,执行权代表着函数内代码是否可以执行以及暂停函数内代码执行的权力,
通过手动操控执行权可以在适当的时机 (例如:接收到请求响应后) 继续执行函数内的代码.

Generator 的使用#

如何创建一个 Generator 函数
Generator 与普通函数最大的区别就是声明一个 Generator 函数时需要带上一个标志 * 号,例如:

function* generator() {}
function * generator() {}
function*generator() {}
function *generator() {}

这四种均为 Generator 函数的书写方式 (无优劣之分), 一般来讲会推荐第零位的这种写法,大多数格式化代码的情况下会把 Generator 格式化为第零位该种代码格式。还需要注意的一点就是 Generator 函数是无法当作构造函数使用的.

Generator 函数的执行特点
当你 Generator 函数执行后你会发现你在该函数内写的任何东西都没有开始执行,这就是你可以管理函数执行权的一种体现。你接收到该函数返回值默认会是一个处于暂停状态的 generator 对象,你可以通过该对象身上的 next () 进行执行一次函数内代码执行权,该次执行的代码范围是:如果是第一次执行则是函数开始到下一次 yield 表达式的位置 /return 位置,其他则是从上一次 yield 表达式位置执行到下一次 yield 表达式位置 /return 位置。每次执行 next () 方法你都会得到一个对象: {value: undefined, done: false}, 该对象中 value 代表着 yield 表达式 /return 表达式的返回值,done 代表是该 Generator 是否为最后一次函数内代码权的执行,true 代表着后续你在使用 next () 函数会是个没有意义的行为,false 则否之.

Generator 执行示意图
Generator 执行示意图

yield 表达式#

从上面对 Generator 介绍及特点,我们可以了解到在 Generator 中 yield 表达式的重要性,yield 在 Generator 中代表着每次代码执行范围 (即从上一个 yield 到该次 yield) 中间的代码,同样既然 yield 是否可以在其他函数内部使用呢?显然是不可以的,因为在其他函数中使用没有存在的意义,因为其他函数是不能控制函数内部执行权的.

其中最需要注意的一点就是 yield 返回值是不会赋值给变量的,yield 值是会传递到 next () 函数执行的返回值出现,你在调用的 next 函数时传递的参数会作为上一次 yield 执行的结果出现在赋值操作上的,这样用语言描述是比较混乱的,下面我们用一张图来进行解释:
yield 与 next 的关系

Generator 与 Interator#

了解 Interator
Interator 是 Es6 中引入的新的遍历机制,该机制具有以下几种特点:

  1. 具有 Symbol.iterator 接口属性
  2. 可使用 forof 遍历 (forof 仅可遍历具有 Interator 接口的数据)
  3. Symbol.iterator 属性返回的函数具有 Generator 特性即控制执行权的生成器
    因为 Generator 就是一个遍历器生成器,所以我们可以给不具有 Symbol.iterator 属性的对象手动的添加一个遍历器接口使得该对象可以使用 forof 遍历以及一些方法,有兴趣可以看一看下面这段代码:
const obj = {};
obj[Symbol.iterator] = function* () {
    yield "CodeGorgeous";
    yield "MaoMao";
    yield "BestFriend";
};
for (const item of arr) {
    console.log(item);// CodeGorgeous // MaoMao // BestFriend
};
const arrObj = [...obj];
console.log(arrObj);// [ 'CodeGorgeous', 'MaoMao', 'BestFriend' ]

Generator 与 throw#

在正常的一段 Js 代码中我们可以通过 trycatch 表达式对一段代码进行包裹以防止当该段代码出现错误时影响到其他代码的执行。如果你编写过 / 看过一些第三库的代码,你有时候会看到例如在一个函数内会首先判定传递进入的参数是否符合预期类型,如果不符合则会对其进行手动使用throw抛出一个错误用于提示开发者,在 Generator 中则另外提供了一个 throw 方法用于在生成器中抛出错误.

function* printStrGenerator() {
    let index = 0;
    while (true) {
        try {
            yield `This is ${index ++}`
        } catch (error) {
            console.log("Generator内部捕获错误", error);
        }
    }
};

const g = printStrGenerator();
console.log(g.next()); // {value: 'This is 0', done: false}
try {
    g.throw(new Error("An expected error occurred in the code")); // Generator内部捕获错误 Error: An expected error occurred in the code
    g.throw(new Error("An expected error occurred in the code")); // Generator内部捕获错误 Error: An expected error occurred in the code
} catch (error) {
    console.log("Window外部捕获错误");
}
console.log(g.next()); // {value: 'This is 3', done: false}

我们从该段例子中可以看出,Generator.prototype.throw 并不只具有抛出错误的功能,同时具有执行一次 next () 的特性,在该段示例中因为我使用的是一个 while 无限循环 + trycatch 使用,所以每次抛出错误都会被 Generator 内部捕获错误 (但是你一定要明确每次这部分都是在不同块级作用域的并不是同一个作用域一直用), 如果你在书写每个 yield 表达式并未被函数内部 trycatch 捕获 / 当前 Generator 已经关闭但是你仍 throw 抛出这两种情况会被外部相应的 trycatch 所捕获 (内部捕获和外部捕获具有完全不同的意义). 还需要注意一点的是 Generator 函数执行过程中内部如果抛出错误但内部未捕获 (ps: 肯定要外部捕获,因为外部要还是不捕获这个程序就挂掉了) 则会终止掉 Generator 的执行如果后续继续执行了 next 则只会返回一个{value: undefined, done: true}, 感觉说着说着又模糊了起来,不妨我们建立一个对照组即可清晰观察到差异:

  • Generator 内部未捕获,由外部捕获
function* printStrGenerator() {
    let index = 0;
    while (true) {
        yield `This is ${index ++}`;
    }
};

const g = printStrGenerator();
try {
    console.log(g.next()); // {value: 'This is 0', done: false}
    console.log(g.throw(new Error("This is Error"))); // 这一步执行会Generator会报错
} catch (error) {
    console.log(g.next()); // {value: undefined, done: true}
    console.log(g); // printStrGenerator {<closed>}
}
  • Generator 内部捕获,外部不捕获
function* printStrGenerator() {
    let index = 0;
    while (true) {
        try {
            yield `This is ${index ++}`;
        } catch (error) {
            console.log(error);
        }
    }
};

const g = printStrGenerator();
console.log(g.next()); // {value: 'This is 0', done: false}
console.log(g.throw(new Error("This is Error"))); // Error: This is Error // {value: 'This is 1', done: false}
console.log(g.next()); // {value: 'This is 2', done: false}
console.log(g); // printStrGenerator {<suspended>}

Generator 与 return#

在正常情况下 return 作为终止函数执行并返回默认返回 undefined 的情况下出现,在此处我们讨论的是 Generator.prototype.return 该方法,为什么我会提及这两个不同层次的东西呢 (ps: 一个语法层次,一个就是个函数), 不过在 Generator 中原型链 (原型链不熟悉的可以看一下额外顺带的知识部分) 的 return 同样能够达到这样的效果,同样我们观察一个例子就能够看出效果:

function* printStrGenerator() {
    let index = 0;
    while (true) {
        yield `This is ${index ++}`;
    }
};

const g = printStrGenerator();

console.log(g.next()); // {value: 'This is 0', done: false}
console.log(g.return("Done")); // {value: 'Done', done: true}
console.log(g.next()); // {value: undefined, done: true}

嵌套式 Generator 的处理方案#

可以想到这么一种场景,我把不同的异步分离抽成了不同的 generator, 我使用的时候放到一个总的 Generator 组合使用管理即可,这样的想法确实很 Nice, 一方面能够想到抽离增强复用性,另一方面组合式使用更便于对逻辑的分离 (不需要在使用该函数时思考其函数内部), 再另一方面就是维护性高当某个 Generator 出问题时正常只需要修复该函数即可不需要再去调整其他地方的代码,好了接下来我们动手先试一试感受一下吧

function* getUserInfoGenerator(generator) {
    return yield (setTimeout(() => {
        const useInfo = {
            nickname: "CodeGorgeous",
            openid: "aszxzxcISadaxzxxxxxx"
        };
        console.log("获取用户信息成功");
        generator && generator(useInfo);
    }, Math.random() * 5000));
}

function* getProjectConfigGenerator(generator) {
    return yield (setTimeout(() => {
        const projectConfig = {
            theme: "default",
            lang: "zh-CN",
            title: "集中一点, 登峰造极⚡"
        };
        console.log("获取项目配置成功");
        generator && generator(projectConfig);
    }, Math.random() * 5000))
}

function* dispatchGenerator() {
    yield "1";
    console.log("开始获取用户信息");
    const useInfo = yield getUserInfoGenerator;
    yield "2";
    console.log(`开始获取的项目配置`, useInfo);
    // console.log(`开始获取${useInfo && useInfo.nickname}用户的项目配置`, useInfo);
    const projectConfig = yield getProjectConfigGenerator;
    console.log(projectConfig);
    yield "3";
}

// 执行调度
const g = dispatchGenerator();
// 这里是纯纯的突发奇想做一个Generator自动执行器(写个这个好折磨人呀, 头发又开始掉光了)

function run(generator) {
    function next(data) {
        var result = generator.next(data);
        if (result.done) return;
        if (typeof result.value === 'function' && result.value[Symbol.toStringTag] === "GeneratorFunction") {
            const temp = result.value(next);
            run(temp);
        } else {
            next();
        }
    }
    next();
};
run(g);

但是在 Generator 中 yield 表达式提供了另一种方式将嵌套式 Generator 扁平化该种方式就是当你碰到一个内嵌 Generator 是可以使用yield*, 我们将上面写的代码稍微加以改造一下

function* getUserInfoGenerator(generator) {
    return yield (setTimeout(() => {
        const useInfo = {
            nickname: "CodeGorgeous",
            openid: "aszxzxcISadaxzxxxxxx"
        };
        console.log("获取用户信息成功");
        generator && generator.next(useInfo);
    }, Math.random() * 5000));
}

function* getProjectConfigGenerator(generator) {
    return yield (setTimeout(() => {
        const projectConfig = {
            theme: "default",
            lang: "zh-CN",
            title: "集中一点, 登峰造极⚡"
        };
        console.log("获取项目配置成功");
        generator && generator.next(projectConfig);
    }, Math.random() * 5000))
}

function* dispatchGenerator() {
    console.log("开始获取用户信息");
    const useInfo = yield* getUserInfoGenerator(g);
    console.log(`开始获取${useInfo.nickname}用户的项目配置`, useInfo);
    const projectConfig = yield* getProjectConfigGenerator(g);
    console.log(projectConfig);
}

// 执行调度
const g = dispatchGenerator();
g.next();

需要注意的一点就是我上面演示的两个例子中对于异步操作的处理是不合理的,虽然能够实现功能,但是真实要应用于异步操作请看Generator 异步场景应用, 我会在此章节重新书写一遍规范的合理的代码.

漫谈 Generator 与协程#

这部分打算再开一个小文章进行记载,因为感觉漫谈着容易谈偏.

Generator 异步场景应用#

Generator 其本身是无法处理异步场景的,其主要功能就是作为异步操作的容器而出现,在平时开发中一般也不会使用 Generator 的,自己平时遇见的一个库 (redux-saga) 其基本就是根据 Generator 进行开发的,有兴趣可以了解一下 React 及其生态圈,那我们平时开发可以通过什么方式解决呢,这时就配合我们最开始的提到的异步解决方案使用,下面我就使用了两种方式进行示例:

  • 解决方案:
    1. 回调函数
      • 回调函数的时机及时交回函数执行权
          function getUserInfoGenerator(callback) {
              return (setTimeout(() => {
                  const useInfo = {
                      nickname: "CodeGorgeous",
                      openid: "aszxzxcISadaxzxxxxxx"
                  };
                  console.log("获取用户信息成功");
                  callback(useInfo);
              }, Math.random() * 5000))
          }
      
          function getProjectConfigGenerator(callback) {
              return (setTimeout(() => {
                  const projectConfig = {
                      theme: "default",
                      lang: "zh-CN",
                      title: "集中一点, 登峰造极⚡"
                  };
                  console.log("获取项目配置成功");
                  callback(projectConfig);
              }, Math.random() * 5000))
          }
      
          function* dispatchGenerator() {
              console.log("开始获取用户信息");
              const useInfo = yield getUserInfoGenerator;
              console.log(`开始获取${useInfo && useInfo.nickname}用户的项目配置`, useInfo);
              const projectConfig = yield getProjectConfigGenerator;
              console.log(projectConfig);
          }
      
          // 执行调度
          const g = dispatchGenerator();
      
          function run(generator) {
              function next(data) {
                  var result = generator.next(data);
                  if (result.done) return;
                  if (typeof result.value === 'function') {
                      result.value((data) => {
                          next(data);
                      });
                  } else {
                      next(result.value);
                  }
              }
              next();
          };
          run(g);
      
    2. Promise
      • 通过 then 的时机及时交回函数执行权
              function getUserInfoGenerator() {
                  return new Promise(resolve => {
                      setTimeout(() => {
                          const useInfo = {
                              nickname: "CodeGorgeous",
                              openid: "aszxzxcISadaxzxxxxxx"
                          };
                          console.log("获取用户信息成功");
                          resolve(useInfo);
                      }, Math.random() * 5000)
                  });
              }
      
              function getProjectConfigGenerator() {
                  return new Promise(resolve => {
                      setTimeout(() => {
                          const projectConfig = {
                              theme: "default",
                              lang: "zh-CN",
                              title: "集中一点, 登峰造极⚡"
                          };
                          console.log("获取项目配置成功");
                          resolve(projectConfig);
                      }, Math.random() * 5000)
                  })
              }
      
              function* dispatchGenerator() {
                  console.log("开始获取用户信息");
                  const useInfo = yield getUserInfoGenerator();
                  console.log(`开始获取${useInfo && useInfo.nickname}用户的项目配置`, useInfo);
                  const projectConfig = yield getProjectConfigGenerator();
                  console.log(projectConfig);
              }
      
              // 执行调度
              const g = dispatchGenerator();
      
              function run(generator) {
                  function next(data) {
                      var result = generator.next(data);
                      if (result.done) return;
                      if (typeof result.value === 'object' && result.value.constructor.name === "Promise") {
                          result.value.then(data => {
                              next(data);
                          });
                      } else {
                          next(result.value);
                      }
                  }
                  next();
              };
              run(g);
      

Generator 与 async#

async 为 ES2017 版本推出的一种函数,async 的出现会让管理异步操作更加方便简洁,为什么这里会从 Generator 讨论到 async 呢?下面我们观察一下下面两种方式:

  • Generator 方式
function getUserInfoGenerator(callback) {
    return (setTimeout(() => {
        const useInfo = {
            nickname: "CodeGorgeous",
            openid: "aszxzxcISadaxzxxxxxx"
        };
        console.log("获取用户信息成功");
        callback(useInfo);
    }, Math.random() * 5000))
}

function getProjectConfigGenerator(callback) {
    return (setTimeout(() => {
        const projectConfig = {
            theme: "default",
            lang: "zh-CN",
            title: "集中一点, 登峰造极⚡"
        };
        console.log("获取项目配置成功");
        callback(projectConfig);
    }, Math.random() * 5000))
}

function* dispatchGenerator() {
    console.log("开始获取用户信息");
    const useInfo = yield getUserInfoGenerator;
    console.log(`开始获取${useInfo && useInfo.nickname}用户的项目配置`, useInfo);
    const projectConfig = yield getProjectConfigGenerator;
    console.log(projectConfig);
}

// 执行调度
const g = dispatchGenerator();

function run(generator) {
    function next(data) {
        var result = generator.next(data);
        if (result.done) return;
        if (typeof result.value === 'object' && result.value.constructor.name === "Promise") {
            result.value.then(data => {
                next(data);
            });
        } else {
            next(result.value);
        }
    }
    next();
};
run(g);
  • async 方式
function getUserInfoGenerator() {
    return new Promise(resolve => {
        setTimeout(() => {
            const useInfo = {
                nickname: "CodeGorgeous",
                openid: "aszxzxcISadaxzxxxxxx"
            };
            console.log("获取用户信息成功");
            resolve(useInfo);
        }, Math.random() * 5000)
    });
}

function getProjectConfigGenerator() {
    return new Promise(resolve => {
        setTimeout(() => {
            const projectConfig = {
                theme: "default",
                lang: "zh-CN",
                title: "集中一点, 登峰造极⚡"
            };
            console.log("获取项目配置成功");
            resolve(projectConfig);
        }, Math.random() * 5000)
    })
}

async function dispatchGenerator() {
    console.log("开始获取用户信息");
    const useInfo = await getUserInfoGenerator();
    console.log(`开始获取${useInfo && useInfo.nickname}用户的项目配置`, useInfo);
    const projectConfig = await getProjectConfigGenerator();
    console.log(projectConfig);
}
dispatchGenerator();

我们可以看出两者书写方式差距体现在:

  1. Generator 函数的符号 * 替换为 async
  2. yield 替换为 await
  3. Generator 函数自动执行完毕需要依靠执行器,async 函数则是直接调用该函数即可
    所以说理论来讲 async 就是 Generator 函数的语法糖,是一种更加精简的写法省去的 Generator 的执行器,所以理论上来讲 Generator 或多或少都是间接性的出现在代码中只是直观没有感受到罢了.
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。