)
教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载导读JavaScript 中的 Promise 天然是异步并发的Promise.all()会让所有异步任务同时启动、竞速完成但在很多业务场景中你需要的是做完一个再做下一个的串行顺序执行。本文基于 30 seconds of code 仓库中的 run-promises-in-series 代码片段深入讲解如何借助Array.prototype.reduce()与Promise.prototype.then()实现 Promise 的链式串行执行并对比for...of await、Promise.all()、forEach()等常见方案的差异读完即可在限流请求、依赖式初始化、顺序 I/O 等场景中直接落地使用。为什么需要串行执行 PromiseJavaScript 的 Promise 是异步的一旦创建其底层异步操作网络请求、文件读写、定时器等会并行启动互不等待。这种并发特性带来了极高的性能但并非所有场景都适合并发依赖关系任务 B 必须使用任务 A 的返回值B 无法提前启动资源限制批量向某个 API 发起请求时并发过多会触发限流rate limit顺序要求日志回放、逐条处理队列、按序写入等场景要求结果严格有序。当上述需求出现时就需要把一串 Promise 变成接力赛上一个任务 resolve 之后下一个任务才开始执行。这正是runPromisesInSeries要解决的问题。核心实现一行代码完成 Promise 串行化30 seconds of code 给出的核心实现极其精简完整代码如下原文见 content/snippets/js/s/run-promises-in-series.mdconst runPromisesInSeries ps ps.reduce((p, next) p.then(next), Promise.resolve());逐行拆解输入约定ps是一个由函数组成的数组而不是 Promise 数组。每个函数被调用后返回一个 Promise或任意值。这非常关键——函数是惰性的只有真正调用时异步操作才会开始从而保证严格顺序。reduce()的初始值Promise.resolve()返回一个已经 fulfilled 的 Promise作为整条链的起点它不会触发任何副作用。累加器preduce()每轮迭代都会把上一个元素产生的 Promise 传给下一轮作为新的p。链接动作p.then(next)当presolve 后立即调用下一个函数nextnext返回的 Promise 成为新的累加器。于是每一步都等待上一步完成才启动形成严格的串行链。用文字描述这条链Promise.resolve().then(fn1).then(fn2).then(fn3)...。reduce()只不过是把这段手写的链动态生成了出来。为什么传入函数数组而非 Promise 数组如果直接传入 Promise 数组所有 Promise 在数组创建那一刻就已经开始执行了reduce 根本来不及拦住它们——顺序执行的前提也就被破坏了。把创建 Promise的动作包进函数即 thunk / 惰性求值是本节目的核心设计之一。可以推断作者如此设计正是为了让异步任务的启动时机完全由链来控制。完整可运行示例结合仓库中 sleep 片段 的延迟函数思路给出一个可直接复制的完整示例const runPromisesInSeries ps ps.reduce((p, next) p.then(next), Promise.resolve()); // 返回一个在 d 毫秒后 resolve 的 Promise参考 sleep 的 Promise 化写法 const delay d new Promise(r setTimeout(r, d)); runPromisesInSeries([() delay(1000), () delay(2000)]); // 每个 Promise 依次执行总耗时约 3 秒在浏览器控制台或 Node.js 中运行可以观察到第二个delay的定时器是在第一个定时器触发后才被创建的整串任务总耗时 1s 2s 3s而非并行的 2s。需要拿到每个任务的返回值怎么办串行场景往往还需要传递数据。只需在链上追加then收集结果即可const runPromisesInSeries ps ps.reduce((p, next) p.then(() next()), Promise.resolve()) .then(() All done); // 若需要逐个拿到结果 const results []; ps.reduce((p, next) p.then(next).then(result { results.push(result); }), Promise.resolve() );注意第一版用p.then(() next())是为了支持不接收上一结果的任务而原版p.then(next)则会把上一个任务的返回值直接作为参数传给下一个函数——两种写法的差异在于任务是否消费前序返回值可依据业务自行选择。错误处理链上任何一个任务失败怎么办顺序链是一条then链因此其错误传播遵循 Promise 链的标准语义。仓库中 promise-then-catch 片段 明确强调链上每个方法都会返回新 Promise错误会沿链传递then与catch的顺序决定最终行为。对应到本片段runPromisesInSeries([ () delay(1000), () Promise.reject(boom), () delay(2000) // 永远不会执行 ]).catch(err console.log(err)); // 输出 boom一旦中间某个 Promise 被 reject后续的then(next)都不会被调用整条链直接进入 rejected 状态需要在失败后继续往下走时应在对应位置插入catch将错误吞掉并返回新值推荐始终在链尾挂一个.catch()统一兜底避免未处理的 rejection。这也是该片段作为通用工具函数能安全复用的前提它把顺序语义和错误语义完全交给了原生 Promise 链不引入任何自定义状态机。对比其他顺序/并发方案30 seconds of code 的 async-array-loops 片段 系统对比了几种异步数组处理方式正好与本片段互为印证方案一for...ofawait顺序const uppercaseItems async () { const items [a, b, c]; for (const item of items) { const uppercaseItem await asyncUppercase(item); // 串行等待 console.log(uppercaseItem); } console.log(Items processed); };await在for/for...of循环中会让代码停住等待当前异步操作完成再继续因此同样实现串行。与runPromisesInSeries相比for...of await写法直观、可读性好且天然支持任意数量的任务runPromisesInSeries是纯函数式工具不依赖async/await语法可在任意环境中作为通用函数传递使用两者原理等价for...of await本质也是上一步完成再启动下一步适合写业务代码runPromisesInSeries适合封装成通用工具。方案二Promise.all()并行Promise.all(items.map(async item ...)); // Logs: A, C, B, Items processed顺序随机Promise.all()会并行执行所有异步操作输出顺序由各任务耗时决定不可预测。因此它只适合无依赖、无顺序要求、且并发量可控的场景——这是与串行方案最本质的差异。方案三forEach()async陷阱await items.forEach(async item { ... }); // Logs: Items processed, B, A, Cawait 形同虚设Array.prototype.forEach()本身是同步方法不会等待async回调完成await items.forEach(...)起不到任何等待作用。async-array-loops明确指出该写法会导致其余代码先执行、异步操作未被等待的 bug。在需要串行时请勿使用forEach()。对比小结方案执行方式是否串行适用场景runPromisesInSeriesreduce 链链式 then是通用工具函数、函数数组、动态任务序列for...ofawait循环等待是业务代码中直观的串行处理Promise.all()并行启动否无依赖、可并发的批量任务forEach()async不等待否且易出 bug不应使用延伸异步函数组合中的同源思想本片段的reduce then思想在仓库中还有更丰富的应用。30 seconds of code 的 async-function-composition 片段 用几乎相同的结构实现了异步函数的管道组合const pipeAsync (...fns) arg fns.reduce((p, f) p.then(f), Promise.resolve(arg)); const sum pipeAsync( x x 1, x new Promise(resolve setTimeout(() resolve(x 2), 1000)), x x 3, async x (await x) 4 ); (await sum(5)); // 15约 1 秒后对比两者runPromisesInSeries的ps.reduce((p, next) p.then(next), Promise.resolve())与pipeAsync的fns.reduce((p, f) p.then(f), Promise.resolve(arg))结构完全同构差异仅在于runPromisesInSeries不传初始值任务之间无参数传递而pipeAsync把arg作为首个 Promise 的 resolve 值实现数据在函数链中流动若任务间需要传递数据可把runPromisesInSeries与pipeAsync合并理解p.then(next)本身就会把上一个 resolve 值传给next只是原片段中的示例任务不消费该参数。顺带一提仓库中的 promisify 片段见 promises 集合可以将 Node 风格回调函数转换为 Promise配合runPromisesInSeries即可对任意回调式异步操作做串行编排。实际应用场景结合以上分析runPromisesInSeries适合以下典型场景接口限流下的批量请求依次请求多个分页数据避免触发 API 频控有依赖的初始化流程先建连接、再查配置、最后加载数据步骤间必须严格有序顺序写入/顺序播放日志逐条落盘、音视频片段按序处理任务队列的轻量实现把待处理任务包装成函数数组用本函数一键按序消费。当任务数量未知或需要动态追加时只需维护一个函数数组在数组上调用本函数即可若任务量大也可结合 async-cheatsheet 等资料设计带并发上限的调度器。小结runPromisesInSeries用一行reduce就把Promise 串行执行这个需求表达得干净利落函数数组保证了任务惰性Promise.resolve()提供了链的起点p.then(next)则把前序完成翻译为后继启动。它不引入额外抽象错误处理完全遵循原生 Promise 链语义并与仓库中for...of await、async-function-composition等片段相互印证。在需要按顺序、一个接一个处理异步任务时它是最简洁可靠的通用答案。赞分享教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载相关推荐30-seconds-of-code 深度解析JavaScript Promise 中 then 与 catch 的顺序为什么至关重要30 seconds of code 深度解析JavaScript Promise 中 then 与 catch 的顺序为什么至关重要 导读 在 JavaSc教程文档30 seconds of code用 reduce 与 slice 一行实现 JavaScript 部分和数组Partial Sum Array30 seconds of code用 reduce 与 slice 一行实现 JavaScript 部分和数组Partial Sum Array 导读教程文档30 seconds of code用 reduce 与 Map 实现 JavaScript 数组分区partition30 seconds of code用 reduce 与 Map 实现 JavaScript 数组分区partition 数组 分区partitioni教程文档创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考