讲故事学 Generator
这篇文章先说一个小故事,作为引子,然后系统讲讲 Generator。阅读这篇文章的话,需要一些前置知识:起码用过 Promise 和 async/await。
小故事
吃货小明去了一家菜鸟镇非常有名的餐馆——《老三样餐馆》,服务员小姐姐给了一份菜单,上面只有三道菜,从上到下列着:红烧肉,辣子鸡,兰州牛肉面。
小明都想试试,但是不知道这个餐馆的菜的饭量。但是小明是个 i 人,服务员小姐姐太漂亮了,一见到她就脸红,更别说开口问菜量了。于是小明就想,吃完一个如果还没吃饱就继续点。
于是,小明就对服务员说照菜单来一个,红烧肉。服务员通知厨师做红烧肉,等了一会儿,服务员小姐姐把菜端上来了,没想到这个红烧肉这么好吃,入口即化,但是就是量太少了,压根就不够塞牙缝的。
接着,小明又对服务员说再来一道菜,辣子鸡。服务员通知厨师做辣子鸡,等了良久,服务员小姐姐把辣子鸡端上来了,小明一看,好家伙,一大盆辣子鸡,看着就很下饭,可劲儿吃,吃到最后就只剩辣椒了,可能是辣味把胃口打开了,但是还没吃饱。。
最后,小明又对服务员说再来一道,兰州牛肉面。服务员通知厨师做兰州牛肉面,等了不一会儿,服务员小姐姐就把兰州牛肉面端上来了,整整一汤碗,香味扑鼻,劲道十足,量还不少。终于让小明吃的饱饱的,还是主食垫肚子。
下饭故事到这里就结束了。用点餐来类比生成器的话,生成器就像是餐厅里的服务员。你告诉服务员你需要什么菜(通过 yield
),服务员记下来然后离开去厨房准备(暂停执行)。当厨房准备好这道菜时(next
方法被调用),服务员会把菜送回来给你,然后等待你的下一道指令。这样,你能够根据餐桌上的需求逐步点菜(逐步生成值),而不是一次性把所有菜都端上来。
下面使用生成器结合 Promise 通过代码来模拟一下点餐过程,演示下 Generator 的用法。你可以把代码复制浏览器 F12 控制台面板直接运行,或者在 node 环境下运行。查看输出结果。
/* 用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
关键字暂停执行,然后在稍后的某个时刻恢复执行。这使得生成器在处理大量数据、实现异步操作、创建迭代器等方面非常有用。
生成器函数的定义使用星号 * 来标识,例如:
function* myGenerator() {
// ...
}
在生成器函数内部,yield
表达式用于暂停函数的执行并返回一个值。当生成器函数被调用时,它不会立即执行,而是返回一个生成器对象。这个对象有一个 next()
方法,每次调用 next()
时,生成器会继续执行,直到遇到下一个 yield
表达式,然后返回一个对象,该对象有一个 value
属性(包含 yield
后的值)和一个 done
属性(表示是否执行完毕)。
例如,以下是一个简单的生成器示例,它生成一个序列:
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
可以看作是对Generator
和Promise
组合使用模式的一种优化和封装。它们都旨在改善 JavaScript 中的异步编程体验,但async/await
提供了更直接、更符合直觉的语法来处理异步操作,减少了手动管理 Promise 链的复杂性。
而且,async/await
的语法更简洁,更接近于同步代码。
await 直接表达了等待异步操作完成的意图,而 Generator 需要额外的机制(如yield
)来迭代和处理Promise
。
所以一般情况下不推荐使用 Generator 来控制 Promise,而是使用 async/await。再拿我们的点菜的例子来看,代码可以按照下面这样改。改完之后async/await
的写法更清晰,更易读,而且我仍然可以点一个吃一个,更重要的是,我可以选择先点哪一个,比如说可以先吃辣子鸡,再吃红烧肉,最后再吃兰州牛肉面。
// 创建一个模拟制作红烧肉的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 的一些常见的一些用法,这里就不一一解释了,大家可以自己试试,有个大概理解再去查阅资料系统学习。
情景一
小胖胃口大,确定能吃完,想一次性同时点红烧肉、辣子鸡、兰州牛肉面,让厨子一起给他做,那样如果人家厨子比较多的话,可以三秒就上齐菜,怎么实现呢?
(async () => {
const resultAll = await Promise.all([dish1, dish2, dish3]);
console.log(resultAll); // 输出结果['花了2秒,做好了红烧肉', '花了3秒,做好了辣子鸡', '花了1秒,做好了兰州牛肉面']
})();
情景二
小红到餐馆的时候,由于客人太多,有的菜原材料可能不够了,但是小红还是期望都尝一尝,哪怕就只能吃到其中一种,小红跟店家说:你都给我先安排上,没有的跟我反馈一下,有原材料的做好了都端上来。这怎么实现呢?
(async () => {
const resultAllSettled = await Promise.allSettled([dish1, dish2, dish3]);
console.log(resultSettle);
})();
打印的是一个数组,里面有 3 个对象,每个对象分别对应一个 Promise,每个对象都有status
和value
属性,status
属性表示 Promise 的状态,value
属性表示 Promise 的返回值。
情景三
小先到餐馆的时候,陆续又到了两个客人,但是店家三样菜都只够各样一份的。小先比较赶时间,就跟店家说:要不这样,你们都做了吧,我要上菜最快的那一个。怎么实现呢?
(async () => {
const resultRace = await Promise.race([dish1, dish2, dish3]);
console.log("最快的:", resultRace); //最快的: 花了1秒,做好了兰州牛肉面
})();
最后
文章系统介绍了 ES6 的生成器 Generator,包含精心设计的故事和实例代码,既生动有趣,有可操作性,也不失深度。创作不易,有收获的话,可以点个赞哟,欢迎留言交流。