面试官:你知道 ES6 的 生成器 Generator 吗?小明:说起 Generator,还得从我上次去餐馆点餐开始说起

讲故事学 Generator

这篇文章先说一个小故事,作为引子,然后系统讲讲 Generator。阅读这篇文章的话,需要一些前置知识:起码用过 Promise 和 async/await。

小故事

吃货小明去了一家菜鸟镇非常有名的餐馆------《老三样餐馆》,服务员小姐姐给了一份菜单,上面只有三道菜,从上到下列着 :红烧肉,辣子鸡,兰州牛肉面。 小明都想试试,但是不知道这个餐馆的菜的饭量。但是小明是个 i 人,服务员小姐姐太漂亮了,一见到她就脸红,更别说开口问菜量了。于是小明就想,吃完一个如果还没吃饱就继续点

于是,小明就对服务员说照菜单来一个 ,红烧肉。服务员通知厨师做红烧肉,等了一会儿,服务员小姐姐把菜端上来了,没想到这个红烧肉这么好吃,入口即化,但是就是量太少了,压根就不够塞牙缝的。

接着,小明又对服务员说再来一道 菜,辣子鸡。服务员通知厨师做辣子鸡,等了良久,服务员小姐姐把辣子鸡端上来了,小明一看,好家伙,一大盆辣子鸡,看着就很下饭,可劲儿吃,吃到最后就只剩辣椒了,可能是辣味把胃口打开了,还是没吃饱。

最后,小明又对服务员说再来一道 ,兰州牛肉面。服务员通知厨师做兰州牛肉面,等了不一会儿,服务员小姐姐就把兰州牛肉面端上来了,整整一汤碗,香味扑鼻,劲道十足,量还不少。终于让小明吃的饱饱的,还是主食垫肚子。

下饭故事到这里就结束了。用点餐来类比生成器的话,生成器就像是餐厅里的服务员。你告诉服务员你需要什么菜(通过 yield),服务员记下来然后离开去厨房准备(暂停执行)。当厨房准备好这道菜时(next 方法被调用),服务员会把菜送回来给你,然后等待你的下一道指令。这样,你能够根据餐桌上的需求逐步点菜(逐步生成值),而不是一次性把所有菜都端上来。

下面使用生成器结合 Promise 通过代码来模拟一下点餐过程,演示下 Generator 的用法。你可以把代码复制浏览器 F12 控制台面板直接运行,或者在 node 环境下运行。查看输出结果。

js 复制代码
/* 用Promise模拟厨师做菜,也类似前端向后端发请求的等待返回结果的过程*/

// 创建一个模拟制作红烧肉的Promise,表示菜品制作需要2秒
const dish1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("花了2秒,做好了红烧肉");
  }, 2000);
});

// 创建一个模拟制作辣子鸡的Promise,表示菜品制作需要3秒
const dish2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("花了3秒,做好了辣子鸡");
  }, 3000);
});

// 创建一个模拟制作兰州牛肉面的Promise,表示菜品制作需要1秒
const dish3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("花了1秒,做好了兰州牛肉面");
  }, 1000);
});

/**
 * 生成器函数,用于生成依次制作的菜品Promise
 * @returns {Object} 生成器对象,每次yield返回一个Promise
 */
function* dishGenerator() {
  yield dish1;
  yield dish2;
  yield dish3;
}

// 初始化生成器,相当于叫来了一个服务员
let genP = dishGenerator();

// 启动菜品制作流程,依次处理每个Promise
genP.next().value.then(res => {
  // 输出第一道菜制作完成的信息
  console.log(res);
  // 继续制作第二道菜
  genP.next(res).value.then(res => {
    // 输出第二道菜制作完成的信息
    console.log(res);
    // 继续制作第三道菜
    genP.next(res).value.then(res => {
      // 输出第三道菜制作完成的信息
      console.log(res);
    });
  });
});

// 输出结果:
// 花了2秒,做好了红烧肉
// 花了3秒,做好了辣子鸡
// 花了1秒,做好了兰州牛肉面

通过上面的小故事,我们对生成器应该有了一个基本的把握,现在系统学习一下生成器。

介绍生成器(Generator)

生成器(Generator)是 JavaScript(从 ES6 开始引入)中的一种特殊函数,它允许你创建可暂停执行的函数。在常规函数中,一旦执行开始,它就会一直运行到结束,但在生成器函数中,你可以使用 yield 关键字暂停执行,然后在稍后的某个时刻恢复执行。这使得生成器在处理大量数据、实现异步操作、创建迭代器等方面非常有用。

生成器函数的定义使用星号 * 来标识,例如:

javascript 复制代码
function* myGenerator() {
  // ...
}

在生成器函数内部,yield表达式用于暂停函数的执行并返回一个值。当生成器函数被调用时,它不会立即执行,而是返回一个生成器对象。这个对象有一个 next()方法,每次调用 next()时,生成器会继续执行,直到遇到下一个 yield 表达式,然后返回一个对象,该对象有一个 value 属性(包含 yield 后的值)和一个 done 属性(表示是否执行完毕)。

例如,以下是一个简单的生成器示例,它生成一个序列:

javascript 复制代码
function* generateNumbers(n) {
  for (let i = 1; i <= n; i++) {
    yield i;
  }
}

let generator = generateNumbers(5);
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
// ...
console.log(generator.next()); // { value: undefined, done: true }

生成器的应用场景

这里只做列举,具体实现的细节,可以自行查阅相关资料,或者用 AI 工具,辅助学习。

  • 生成器可以用于模拟异步操作,例如读取文件、网络请求等。
  • 懒加载和流式处理。在某些前端路由库或服务器端渲染框架中,生成器可以用于构建中间件链,允许在执行过程中暂停和恢复。
  • 动画和定时任务:生成器可以用于控制动画帧,或者在间隔时间内执行任务,这样可以更好地控制时间间隔和任务执行顺序。
  • 构建工具:在构建工具如 Gulp 或 Webpack 中,生成器可以用于自定义构建流程,例如按需编译文件或处理依赖关系。
  • 状态管理:在状态管理库(如 Redux Saga)中,生成器用于处理副作用和异步操作,提供了一种优雅的方式来管理应用的状态和副作用。

async/await 解决回调地域

我们在上面使用生成器(Generator)和 Promise 组合的方式实现了模拟点餐的代码,其中为了依次获取 Promise 的异步回调的执行结果 resolve中的值,我们写了一个类似套娃的回调地域

这样的代码会非常不美观,影响代码的可阅读性,如果是在项目中会降低代码的可维护性。为了解决这个问题,我们可以使用ES8的语法糖 async/await

async/await可以看作是对GeneratorPromise组合使用模式的一种优化和封装。它们都旨在改善 JavaScript 中的异步编程体验,但async/await提供了更直接、更符合直觉的语法来处理异步操作,减少了手动管理 Promise 链的复杂性。

而且,async/await的语法更简洁,更接近于同步代码。 await 直接表达了等待异步操作完成的意图,而 Generator 需要额外的机制(如yield)来迭代和处理Promise

所以一般情况下不推荐使用 Generator 来控制 Promise,而是使用 async/await 。再拿我们的点菜的例子来看,代码可以按照下面这样改。改完之后async/await的写法更清晰,更易读,而且我仍然可以点一个吃一个,更重要的是,我可以选择先点哪一个,比如说可以先吃辣子鸡,再吃红烧肉,最后再吃兰州牛肉面。

js 复制代码
// 创建一个模拟制作红烧肉的Promise,表示菜品制作需要2秒
const dish1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("花了2秒,做好了红烧肉");
  }, 2000);
});

// 创建一个模拟制作辣子鸡的Promise,表示菜品制作需要3秒
const dish2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("花了3秒,做好了辣子鸡");
  }, 3000);
});

// 创建一个模拟制作兰州牛肉面的Promise,表示菜品制作需要1秒
const dish3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("花了1秒,做好了兰州牛肉面");
  }, 1000);
});
// 立即执行函数
(async () => {
  const res1 = await dish2;
  console.log(res1);
  const res2 = await dish1;
  console.log(res2);
  const res3 = await dish3;
  console.log(res3);
})();

最后小小拓展一下

主要是借助这个点餐的情景,巩固 Promise 的一些常见的一些用法,这里就不一一解释了,大家可以自己试试,有个大概理解再去查阅资料系统学习。

情景一

小胖胃口大,确定能吃完,想一次性同时点红烧肉、辣子鸡、兰州牛肉面,让厨子一起给他做,那样如果人家厨子比较多的话,可以三秒就上齐菜,怎么实现呢?

js 复制代码
(async () => {
  const resultAll = await Promise.all([dish1, dish2, dish3]);
  console.log(resultAll); // 输出结果['花了2秒,做好了红烧肉', '花了3秒,做好了辣子鸡', '花了1秒,做好了兰州牛肉面']
})();

情景二

小红到餐馆的时候,由于客人太多,有的菜原材料可能不够了,但是小红还是期望都尝一尝,哪怕就只能吃到其中一种,小红跟店家说:你都给我先安排上,没有的跟我反馈一下,有原材料的做好了都端上来。这怎么实现呢?

js 复制代码
(async () => {
  const resultAllSettled = await Promise.allSettled([dish1, dish2, dish3]);
  console.log(resultSettle);
})();

打印结果: 打印的是一个数组,里面有 3 个对象,每个对象分别对应一个 Promise,每个对象都有statusvalue属性,status属性表示 Promise 的状态,value属性表示 Promise 的返回值。

情景三

小先 到餐馆的时候,陆续又到了两个客人,但是店家三样菜都只够各样一份的。小先比较赶时间,就跟店家说:要不这样,你们都做了吧,我要上菜最快的那一个。怎么实现呢?

js 复制代码
(async () => {
  const resultRace = await Promise.race([dish1, dish2, dish3]);
  console.log("最快的:", resultRace); //最快的: 花了1秒,做好了兰州牛肉面
})();

最后

文章系统介绍了 ES6 的生成器 Generator,包含精心设计的故事和实例代码,既生动有趣,有可操作性,也不失深度。创作不易,有收获的话,可以点个赞哟,欢迎留言交流。

相关推荐
Hello-Mr.Wang1 分钟前
vue3中开发引导页的方法
开发语言·前端·javascript
程序员凡尘29 分钟前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js
编程零零七4 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
北岛寒沫5 小时前
JavaScript(JS)学习笔记 1(简单介绍 注释和输入输出语句 变量 数据类型 运算符 流程控制 数组)
javascript·笔记·学习
everyStudy5 小时前
JavaScript如何判断输入的是空格
开发语言·javascript·ecmascript
(⊙o⊙)~哦6 小时前
JavaScript substring() 方法
前端
无心使然云中漫步6 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者6 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_7 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js
麒麟而非淇淋8 小时前
AJAX 入门 day1
前端·javascript·ajax