ES6生成器异步流程实战:构建随机数Web服务

发布时间:2026/9/3 11:07:49
ES6生成器异步流程实战:构建随机数Web服务 在 Web 应用开发相关的练习和竞赛题里ES6 生成器Generator并不是一个高频但非常重要的考点。很多人在学习阶段只记住了function*和yield的语法却没有真正把它放进一个异步流程中使用。这篇博文结合“蓝桥杯真题模拟解析”中“用 ES6 生成器异步产生随机数”的考点用一个可运行的 Node.js Web 服务把生成器、异步流程和随机数生成串起来。读完并动手运行后你能理解生成器的执行机制也能在真题模拟、课程作业或练习项目中正确使用它。这篇文章会先解释题目的核心考点然后补齐 ES6 生成器的基础语法接着设计一个基于 Express 的 Web 接口用生成器分步骤异步产生随机数最后给出运行验证、常见报错排查和生产实践建议。整个示例不依赖复杂框架尽量用最小代码跑通完整流程。1. 先把题目背景和核心考点说清楚1.1 这道题到底在考什么“Web 应用开发实战”方向的蓝桥杯类模拟题通常会给你一个很小的 Web 应用场景然后要求用 JavaScript 的某个特性实现业务逻辑。表面上看题目要求的是“异步产生随机数”但实际考核点有三个层次。第一层是 ES6 生成器语法也就是function*、yield、next()三个核心概念是否理解。第二层是异步流程控制在生成器内部如何暂停、恢复、传值以及它和回调、Promise、async/await的关系。第三层是 Web 应用集成也就是生成器最终要在一个真实的接口请求中工作而不是在控制台里单独运行。所以如果你只是会写一个function*然后手动调用几次next()还不足以应对这类题目。你需要把生成器放到一个会被多次请求、有状态管理的 Web 服务里并且正确输出结果。1.2 Generator 为什么能跟“异步”和“随机数”产生关系Generator 的核心能力是“函数可以被暂停”暂停点就是yield表达式。暂停之后外部代码可以决定什么时候让它继续执行。这个能力天然适合异步流程。随机数这个业务点本质上是一个“会产生不同结果”的数据源。假如题目要求每次从生成器中取出一个随机数并且每次取数之间需要模拟网络延迟或数据库查询那么用 Generator 可以把“产生随机数”的过程拆成多个阶段每个阶段都可以暂停等待。举个例子一个接口需要返回三组随机数每组之间要停顿一段时间。最直接的写法是写多个setTimeout回调但逻辑会嵌套。使用 Generator 可以这样理解生成器保存了“取第一个随机数 - 等待 - 取第二个随机数 - 等待 - 取第三个随机数”的完整步骤外部驱动代码负责在每一步暂停时等待相应时间再调用next()继续。1.3 本文要完成的最小可运行场景为了让内容具体可落地本文设计一个最小场景提供一个 HTTP 接口GET /api/random-sequence接口内部使用 ES6 Generator 维护一个随机数生成流程。每次请求会依次产生 3 个随机数每个随机数之间用setTimeout模拟 300ms 的异步耗时。最终以 JSON 格式返回生成的随机数数组和每步状态。学习环境下你只需要一个 Node.js 环境和 Express 依赖。生产环境下你还需要考虑超时、日志、参数校验、随机数安全等问题这些会在第 6 节展开。2. ES6 Generator 语法基础先解决哪些写法容易混淆2.1 function*、yield、next() 的最小例子先看一个最基础的生成器函数。function* numberGenerator() { console.log(开始执行); yield 1; console.log(第一次恢复); yield 2; console.log(第二次恢复); yield 3; console.log(第三次恢复); } const gen numberGenerator(); console.log(gen.next()); console.log(gen.next()); console.log(gen.next()); console.log(gen.next());控制台执行结果会是这样开始执行 { value: 1, done: false } 第一次恢复 { value: 2, done: false } 第二次恢复 { value: 3, done: false } 第三次恢复 { value: undefined, done: true }这里的关键点有三个。第一调用numberGenerator()时函数体内部代码并不会立即执行只有第一次调用.next()时代码才会执行到第一个yield并暂停。第二next()的返回值是一个对象包含value和done两个属性。value是当前yield后面跟的值done表示生成器是否已经执行完毕。第三最后一个next()返回{ value: undefined, done: true }说明生成器已经结束了。如果不判断done很容易多做一次多余调用。2.2 yield 表达式的值和 next() 的参数是同一个通道很多新手不理解yield本身也有值。这个值不是yield后面的数据而是下一次调用next()时传入的参数。看这个例子。function* calculator() { const first yield 请输入第一个数; const second yield 请输入第二个数; yield first second; } const gen calculator(); console.log(gen.next()); console.log(gen.next(10)); console.log(gen.next(20)); console.log(gen.next());输出结果{ value: 请输入第一个数, done: false } { value: 请输入第二个数, done: false } { value: 30, done: false } { value: undefined, done: true }第一次next()让函数执行到const first yield ...这行yield后面的字符串成为value。第二次next(10)传入的10会被赋值给first然后继续执行到第二个yield。第三次next(20)传入的20赋值给second然后执行yield first second得到30。这一机制是生成器实现异步流程控制的基础外部代码可以在异步回调完成时把结果作为参数传回生成器内部。2.3 最容易混淆的三个点第一生成器函数和普通函数不一样。普通函数调用一次执行到底生成器函数调用后只是创建生成器对象不会立即执行。第二yield后面的值是next()返回结果中的value而yield表达式的值来自下一次next()的参数。这两个方向很容易被记反。第三done为true以后不应该继续调用next()并依赖返回值。在循环遍历时需要先判断done再决定是否退出。下面用表格总结一下。操作作用返回值注意点function*声明生成器函数生成器对象调用不会执行函数体yield暂停执行并交出值等待下一次next()传入参数函数体停留在 yield 行next(param)恢复执行{ value, done }传入参数会成为 yield 表达式的值done表示生成器是否结束布尔值结束时不应继续依赖 valuereturn提前结束生成器设置最终 value 和 done结束后再 next 会返回{ value: undefined, done: true }这个基础很重要。下面实现异步随机数服务时会用到next(param)传参机制。3. 设计一个基于 Generator 的异步随机数服务3.1 明确需求和输出结构在写代码之前先把需求说清楚。接口地址是GET /api/random-sequence。每次请求需要返回一个对象包含三部分内容randomNumbers依次生成的随机数数组例如[42, 17, 88]。steps每一步的状态记录包括每次生成随机数时的时间戳和说明。totalTime整个生成流程的耗时单位是毫秒。异步体现在哪里为了体现真正的异步效果设计两个异步阶段。第一步生成随机数前先调用一个返回 Promise 的fetchRandomNumber函数内部用setTimeout模拟 300ms 延迟。第二步生成完一个随机数后再调用一个delay函数等待指定时间后继续下一步流程。这里的随机数并不是真正意义上的“安全随机数”因为Math.random()本身不适用于密码学场景。但在学习练习题中它足以演示生成器与异步流程配合。生产环境建议使用crypto.randomInt这个在第 6 节展开。3.2 项目结构与依赖新建一个项目目录例如generator-random-web然后初始化项目。mkdir generator-random-web cd generator-random-web npm init -y npm install express项目目录结构如下generator-random-web/ ├── package.json ├── server.js └── generator/ └── randomSequence.jsserver.js是 Express 服务入口负责接收 HTTP 请求调用生成器逻辑并返回 JSON。generator/randomSequence.js是生成器相关逻辑把异步随机数生成流程封装成一个函数。如果原始项目没有指定 Node.js 版本建议使用 Node.js 18 及以上因为后面如果需要使用fetch或较新的 npm 依赖会更方便。实际开发中要结合团队统一版本确认。3.3 用 Generator 保存随机数生成状态先实现一个基础版生成器保存“生成几个随机数”的状态。// generator/randomSequence.js function* randomNumberSequence(count, min 1, max 100) { const numbers []; for (let i 0; i count; i) { const random Math.floor(Math.random() * (max - min 1)) min; numbers.push(random); yield { index: i 1, random, numbers }; } return { final: true, numbers }; }这个生成器每次yield时返回当前第几个随机数、本次生成的随机数以及已生成数组的快照。最后一次返回final: true。但这里有一个问题numbers数组是生成器内部维护的。直接把这个生成器交给 Express 路由去调用时外部驱动代码不知道该在什么时候调用next()也不知道异步等待多久。所以还需要一个驱动函数把生成器的每一步转换为异步流程。3.4 用 Generator 驱动异步流程模拟顺序生成下面实现一个更贴近题目要求的生成器。它不只是产生随机数还要在异步流程中工作。// generator/randomSequence.js function delay(ms) { return new Promise((resolve) { setTimeout(resolve, ms); }); } function fetchRandomNumber(min, max) { return delay(300).then(() { return Math.floor(Math.random() * (max - min 1)) min; }); } function* randomSequenceWithAsyncSteps(count, min 1, max 100) { const numbers []; const steps []; for (let i 0; i count; i) { const random yield fetchRandomNumber(min, max); numbers.push(random); steps.push({ step: i 1, value: random, time: new Date().toISOString(), }); yield delay(200); } return { numbers, steps }; }这个生成器的执行流程是这样的。第一次调用next()时函数执行到yield fetchRandomNumber(min, max)此时fetchRandomNumber返回一个 Promise这个 Promise 作为value被外部拿到。外部等待 300ms 后调用next(randomValue)把真正的随机数传回生成器赋值给random。然后生成器把随机数放入numbers记录步骤再执行到yield delay(200)再次暂停。外部等 200ms 后调用next()进入下一轮循环。这里就体现了 Generator 在一个异步流程里的价值生成器负责规划整个流程外部的驱动代码负责处理异步等待和参数回传。接下来需要一个驱动函数把生成器的暂停和恢复串联起来。// generator/randomSequence.js function runGenerator(gen) { return new Promise((resolve, reject) { function step(method, arg) { let result; try { result gen[method](arg); } catch (err) { reject(err); return; } const { value, done } result; if (done) { resolve(value); return; } if (value instanceof Promise) { value .then((res) step(next, res)) .catch((err) { gen.throw(err); reject(err); }); } else { step(next, undefined); } } step(next); }); }这个驱动函数是理解 Generator 异步流程控制的关键。它的逻辑是取出next()返回的value如果value是 Promise就等它完成后把结果通过next(res)传回生成器如果不是 Promise就直接调用next()。整个过程持续到done为true为止。如果暂时不理解这个驱动函数的内部实现可以先把它当作一个工具函数只需要知道给它一个生成器对象它会自动处理 Promise 和next()的传值。等到调试时把中间日志打印出来就清楚多了。3.5 在 Express 路由中接入生成器现在创建server.js把这个生成器接入 Express 路由。// server.js const express require(express); const { randomSequenceWithAsyncSteps, runGenerator, } require(./generator/randomSequence); const app express(); const port 3000; app.get(/api/random-sequence, async (req, res) { const start Date.now(); try { const gen randomSequenceWithAsyncSteps(3, 1, 100); const result await runGenerator(gen); res.json({ code: 0, message: success, data: { randomNumbers: result.numbers, steps: result.steps, totalTime: Date.now() - start, }, }); } catch (err) { console.error(err); res.status(500).json({ code: 500, message: server error, }); } }); app.listen(port, () { console.log(Server is running at http://localhost:${port}); });这里有一个非常重要的工程细节每次请求都要重新创建一个生成器实例randomSequenceWithAsyncSteps(3, 1, 100)不能在模块顶层保存一个单例生成器。因为生成器是有状态的如果多个用户同时请求共享同一个生成器对象会导致状态错乱。3.6 完整示例代码把两个文件完整贴出来方便直接运行。generator/randomSequence.jsfunction delay(ms) { return new Promise((resolve) { setTimeout(resolve, ms); }); } function fetchRandomNumber(min, max) { return delay(300).then(() { return Math.floor(Math.random() * (max - min 1)) min; }); } function* randomSequenceWithAsyncSteps(count, min 1, max 100) { const numbers []; const steps []; for (let i 0; i count; i) { const random yield fetchRandomNumber(min, max); numbers.push(random); steps.push({ step: i 1, value: random, time: new Date().toISOString(), }); yield delay(200); } return { numbers, steps }; } function runGenerator(gen) { return new Promise((resolve, reject) { function step(method, arg) { let result; try { result gen[method](arg); } catch (err) { reject(err); return; } const { value, done } result; if (done) { resolve(value); return; } if (value instanceof Promise) { value .then((res) step(next, res)) .catch((err) { step(throw, err); reject(err); }); } else { step(next, undefined); } } step(next); }); } module.exports { delay, fetchRandomNumber, randomSequenceWithAsyncSteps, runGenerator, };server.jsconst express require(express); const { randomSequenceWithAsyncSteps, runGenerator, } require(./generator/randomSequence); const app express(); const port 3000; app.get(/api/random-sequence, async (req, res) { const start Date.now(); try { const gen randomSequenceWithAsyncSteps(3, 1, 100); const result await runGenerator(gen); res.json({ code: 0, message: success, data: { randomNumbers: result.numbers, steps: result.steps, totalTime: Date.now() - start, }, }); } catch (err) { console.error(err); res.status(500).json({ code: 500, message: server error, }); } }); app.listen(port, () { console.log(Server is running at http://localhost:${port}); });运行命令node server.js启动成功后控制台会输出Server is running at http://localhost:3000这样整个最小可运行闭环已经完成。下一节验证接口输出。4. 运行这个 Web 应用并验证生成结果4.1 启动服务和请求接口服务启动后在另一个终端窗口发送请求。curl http://localhost:3000/api/random-sequence也可以用浏览器直接访问http://localhost:3000/api/random-sequence。接口返回的是 JSON不同的浏览器显示效果不一样不影响结果。如果使用命令行建议安装或使用系统的curl。输出的 JSON 可能像下面这样。{ code: 0, message: success, data: { randomNumbers: [78, 23, 56], steps: [ { step: 1, value: 78, time: 2025-01-04T08:30:00.000Z }, { step: 2, value: 23, time: 2025-01-04T08:30:00.500Z }, { step: 3, value: 56, time: 2025-01-04T08:30:01.000Z } ], totalTime: 1501 } }注意随机数、时间戳、totalTime每次请求都会不同。上面的时间戳间隔大约 500ms这就是 300ms 随机数生成延迟加 200ms 步骤间隔延迟带来的效果。4.2 正常输出结果分析从输出结构可以看到整个请求大约耗时 1500ms 上下。这个值不完全等于三次 300ms 加两次 200ms 的和因为 Node.js 事件循环和setTimeout的定时精度会有微小偏差同时 JSON 序列化和路由处理也有开销。randomNumbers是最终生成的随机数数组。steps记录了每一步的时间和值方便排查生成流程中哪一步卡住。这里有一个值得注意的细节randomNumbers是生成器内部维护的数组通过最后return { numbers, steps }返回。runGenerator在done为true时使用resolve(value)这里的value就是生成器return出来的对象。4.3 按真题模拟解析的思路拆解执行过程如果考试题要求你手工推导执行过程可以按下面步骤分析。假设randomSequenceWithAsyncSteps(3, 1, 100)被创建。第一次next()生成器开始执行进入for循环的第一次迭代。执行到yield fetchRandomNumber(min, max)。fetchRandomNumber返回 Promise该 Promise 作为value返回给外部。生成器暂停尚未产生随机数。外部拿到 Promise 后等待 300ms。Promise 完成后得到随机数78驱动函数调用next(78)。第二次next(78)78被赋值给random。numbers.push(78)。记录steps中的第一条。执行到yield delay(200)返回 Promise。生成器暂停。外部等待 200ms 后驱动函数调用next()。第三次next()进入第二次for循环。执行到yield fetchRandomNumber(min, max)。生成器再次暂停等待随机数。后面重复前两步直到第三次随机数生成并记录完成。最后一次next()使循环结束执行return { numbers, steps }done变为true。驱动函数拿到最终对象并 resolve。这个推导过程就是真题模拟解析中最常考的内容能描述清楚生成器什么时候暂停、什么时候恢复、外部如何传值。只要掌握这个链路核心考点就算真正理解了。5. 常见报错和排查链路5.1 输出一直停在第一个 yield现象接口迟迟不返回或者控制台只打印了第一行日志就卡住。可能原因驱动函数没有处理 Promise或者调用next()的方式不对。比如直接写成const value gen.next(); const random value.value; // 拿到的是 Promise而不是随机数这里value.value是 Promise 对象不是随机数。如果直接把它 push 进数组得到的是[object Promise]一样的结果。检查方式在路由里打印value.value的类型。console.log(value.value instanceof Promise);如果打印结果是true说明你要对 Promise 做then处理或者使用runGenerator驱动函数。解决方案把生成器对象交给runGenerator处理让它自动识别 Promise。预防建议不要在 Express 路由里手动多次调用gen.next()除非你完全清楚每一步返回的是值还是 Promise。统一使用驱动函数可以降低出错概率。5.2 next() 参数传不进去现象生成了随机数但numbers数组中全是undefined。可能原因生成器内部写了const random yield fetchRandomNumber(...)但外部没有把 Promise 的结果通过next(random)传回去而是直接调用了next()。这样random得到的值就是undefined。检查方式在生成器内部插入日志const random yield fetchRandomNumber(min, max); console.log(拿到随机数:, random);如果日志输出拿到随机数: undefined说明传参链路断了。解决方案驱动函数必须在 Promise resolve 后调用gen.next(res)而不是调用gen.next()。看下面这段正确的驱动逻辑if (value instanceof Promise) { value .then((res) step(next, res)) .catch((err) step(throw, err)); } else { step(next, undefined); }预防建议记住yield表达式的值来自下一次next()的参数。你要等 Promise 完成后把结果作为参数传回去。5.3 接口返回空数据或异步顺序乱现象接口能返回但randomNumbers为空数组或者steps顺序和随机数顺序对不上。可能原因创建生成器后没有调用runGenerator而是直接await gen.next()。生成器只执行到第一个yield就停了没有走完整流程。检查方式打印result查看是否是{ numbers: [], steps: [] }。解决方案确保路由里调用的是const gen randomSequenceWithAsyncSteps(3, 1, 100); const result await runGenerator(gen);预防建议把生成器逻辑和路由逻辑分开。生成器只负责描述流程路由只负责请求runGenerator不要把next()的调用散落在路由里。5.4 没有把 Generator 状态隔离导致的并发问题现象多个用户同时请求A 用户返回结果中出现了 B 用户的随机数或者步骤互相穿插。可能原因把生成器对象放在模块全局变量中。例如// 错误示例 const sharedGen randomSequenceWithAsyncSteps(3, 1, 100); app.get(/api/random-sequence, async (req, res) { const result await runGenerator(sharedGen); res.json(result); });生成器是带状态的对象。多个请求共享同一个生成器时A 请求调用一次next()B 请求再调用一次next()状态就乱了。检查方式在路由入口打印生成器对象地址确认每次请求是否复用同一对象。解决方案每个请求都重新创建生成器实例。app.get(/api/random-sequence, async (req, res) { const gen randomSequenceWithAsyncSteps(3, 1, 100); const result await runGenerator(gen); res.json(result); });预防建议把生成器看作“一次性流程对象”用完即弃。如果希望复用参数可以把参数保存在普通变量中。5.5 排查清单问题现象检查顺序检查命令或位置处理建议接口不返回1. Node 服务是否启动node server.js重新启动接口不返回2. 生成器是否停在 Promise 上打印value类型确保 Promise 被 await 或 then随机数为 undefined1.next()是否传参检查驱动函数通过next(res)传值返回空数组1. 是否调用runGenerator检查路由代码使用驱动函数走完整流程并发数据错乱1. 生成器是否全局单例检查生成器创建位置每次请求重新创建接口 5001. 堆栈信息查看终端错误日志按异常信息修复耗时明显过长1. 是否多个延迟叠加totalTime和steps.time检查模板延时时间这里特别提醒一点runGenerator中的catch逻辑应该在 Promise reject 时调用gen.throw(err)让生成器内部也能捕获异常而不是只 reject 外层 Promise。这样才能保证生成器内部的try/catch生效。6. 从真题模拟到生产实践还能怎么扩展6.1 学习环境、测试环境、生产环境的差异在蓝桥杯类真题模拟中重点是快速实现功能验证语言特性代码可以写得比较直接。但在真实 Web 项目里这个“生成器异步产生随机数”的功能还需要考虑更多内容。学习环境以满足题目要求为主使用Math.random()和setTimeout足够。测试环境需要加入参数边界测试例如count等于 0、min大于max、随机数上限过大等场景。生产环境还要考虑接口超时、并发控制、日志采集、监控告警和随机数来源安全。对于这个接口生产环境最直接的改动就是把Math.random()换成crypto.randomInt因为Math.random()不是密码学安全随机数在抽奖、令牌、验证码等场景中不能使用。6.2 随机数安全与并发控制先看crypto.randomInt的用法。const crypto require(crypto); function secureRandomNumber(min, max) { return crypto.randomInt(min, max 1); }crypto.randomInt使用操作系统的随机源生成的随机数分布更均匀也更能抵抗预测性攻击。并发控制方面当前示例中每个请求都会执行约 1.5 秒的异步流程。如果并发很高Node.js 进程的 CPU 和内存占用会上升。生产环境可以按业务需要增加限流中间件比如express-rate-limit防止接口被频繁调用。const rateLimit require(express-rate-limit); const limiter rateLimit({ windowMs: 60 * 1000, max: 30, message: { code: 429, message: too many requests }, }); app.use(/api/random-sequence, limiter);这段代码会把单个 IP 每分钟的请求数限制为 30 次。窗口大小、请求上限要根据业务场景调整不要直接照抄。6.3 从 Generator 到 async/await 的演进面试或真题中经常有一个追问Generator 不是最早解决 Node.js 异步回调嵌套的方案吗为什么现在更多人用async/await原因在于async/await是更符合直觉的语法。生成器本身需要外部驱动函数像上面的runGenerator一样每次都要处理 Promise、.next()传值和错误传播。而async/await把这一套流程内置到了 JavaScript 引擎中。用async/await重写这个随机数流程会简单很多。async function randomSequenceAsync(count, min 1, max 100) { const numbers []; const steps []; for (let i 0; i count; i) { const random await fetchRandomNumber(min, max); numbers.push(random); steps.push({ step: i 1, value: random }); await delay(200); } return { numbers, steps }; }但这不意味着 Generator 不值得学。生成器的“手动暂停/恢复”能力在流式数据处理、惰性求值、复杂状态机等场景中仍然有用。在真题中如果题目明确要求使用 ES6 Generator那就要按本章示例的方式实现如果只是要求实现功能那么async/await是更高效的选择。用表格对比一下三者的适用场景。方式暂停能力异步流程表达能力学习成本推荐使用场景Generator支持手动控制需要驱动函数较高真题考点、状态机、惰性序列Promise不支持暂停链式调用中等单次异步操作、接口请求async/await伪暂停直观同步风格较低大多数业务异步流程6.4 真题应试时的做题顺序建议如果看到类似题目建议按以下顺序完成。先读题确定三个关键信息是否需要实现 Web 接口、是否强制使用 Generator、期望的输出结构是什么。很多考生踩坑的原因是接口实现了但输出结构不对或者用了async/await但没有体现 Generator 考点。然后写最小生成器函数先在控制台手动调用两次next()确认生成器本身能产出随机数。这一步不要急着写 Express 路由。接着编写驱动函数把 Promise 流转和next()传参跑通。用日志打印每一步的value和done确认yield表达式能正确拿到外部传入的随机数。最后再接入 Express 路由处理 JSON 响应和异常分支。这样一层一层调试比一次性写完再定位问题要快得多。6.5 可复用清单在发布或提交代码前按下面清单检查。每次请求是否创建新的生成器实例而非共享全局生成器。是否使用驱动函数处理 Promise 和next()传参而不是在路由内手动调用。是否处理了done为true的结束分支。接口返回结构是否固定是否包含code、message、data。随机数区间参数是否做了边界校验比如min max、count 0。是否在生产环境中使用crypto.randomInt替代Math.random()。是否记录足够日志包括开始时间、结束时间、总耗时和错误堆栈。是否有接口限流、超时控制和异常兜底。是否考虑过并发请求下的状态隔离。是否在提交前删除调试日志和临时注释。这份清单同时适用于真题模拟代码提交和个人项目上线前自查。最后补一个练习建议题目跑通后可以自己再加两个小改动。第一个改动是把随机数生成数量改为从查询参数传入例如/api/random-sequence?count5测试生成器能否根据外部参数控制循环次数。第二个改动是在生成器内部加一次try/catch在 Promise reject 时观察gen.throw(err)是否正确把异常传回生成器内部。这两个练习能帮你把生成器从“会写”变成“用得明白”。

相关新闻