es-toolkit forEachAsync 深入指南异步遍历数组与并发控制完整实战【免费下载链接】es-toolkitA modern JavaScript utility library thats 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit导读forEachAsync是 es-toolkit 数组模块es-toolkit/array提供的异步遍历工具它对数组的每个元素执行一个返回 Promise 的非同步回调函数并在所有异步操作全部完成时返回一个 Promise。与原生forEach不同它会真正等待每个异步任务结束同时通过可选的concurrency选项你可以在不引入额外依赖的情况下限制并发数轻松控制服务器或数据库的负载。读完本文你将掌握forEachAsync的完整 API、并发控制原理信号量 Semaphore、错误传播行为以及它在日志记录、文件上传、数据库批量更新等场景中的最佳实践。一、为什么需要 forEachAsyncJavaScript 原生数组的forEach是一个同步方法它不会等待回调函数中返回的 Promise// 原生 forEach不会等待异步操作完成 users.forEach(async user { await updateUser(user.id); // 这里的 await 对外层毫无意义 }); console.log(可能在任何用户更新完成前就执行了);这会导致回调仍在执行代码却已继续往下走的经典问题。forEachAsync解决了这一痛点——它返回一个 Promise只有所有元素的异步操作都完成后才会 resolve从而保证后续代码如更新完成提示、数据刷新在正确的时机执行。二、基本用法2.1 函数签名await forEachAsync(array, callback);完整签名如下来自 src/array/forEachAsync.tsexport async function forEachAsyncT( array: readonly T[], callback: (item: T, index: number, array: readonly T[]) Promisevoid, options?: ForEachAsyncOptions ): Promisevoid2.2 入门示例批量更新用户信息import { forEachAsync } from es-toolkit/array; // 更新所有用户信息 const users [{ id: 1 }, { id: 2 }, { id: 3 }]; await forEachAsync(users, async user { await updateUser(user.id); }); // 到这里时所有用户更新操作均已全部完成注意回调函数会收到与原生forEach一致的三个参数当前元素item、元素索引index、原始数组array。测试 forEachAsync.spec.ts 中验证了这一点const arr [1, 2, 3]; await forEachAsync(arr, callback); // callback.mock.calls[0] [1, 0, arr] // callback.mock.calls[1] [2, 1, arr] // callback.mock.calls[2] [3, 2, arr]2.3 限制并发数const items [1, 2, 3, 4, 5]; await forEachAsync(items, async item await processItem(item), { concurrency: 2 }); // 同一时刻最多只有 2 个 item 被处理concurrency选项用于控制同时运行的最大操作数防止瞬时大量请求压垮服务器或数据库。它非常适合日志记录、文件上传、数据库更新这类不需要返回值的副作用操作。2.4 串行执行将concurrency设为1即可实现严格的顺序执行——一次只处理一个元素处理完上一个才开始下一个import { forEachAsync } from es-toolkit/array; // 顺序上传文件 const files [file1.txt, file2.txt, file3.txt]; await forEachAsync(files, async file await uploadFile(file), { concurrency: 1 }); // 每个时刻只有一个文件在上传这在处理存在顺序依赖如依赖前一步结果的状态流转或需要温和地控制速率rate limiting的场景下非常实用。三、参数详解参数类型是否必填说明arrayreadonly T[]必填需要遍历的数组callback(item: T, index: number, array: readonly T[]) Promisevoid必填对每个元素执行的异步函数接收当前元素、索引与原始数组optionsForEachAsyncOptions可选控制并发的配置对象options.concurrencynumber可选同时运行的最大操作数不指定时所有操作同时执行即完全并发返回值Promisevoid——当所有操作都完成时 resolve 的 Promise。这意味着你可以直接await forEachAsync(...)也可以将它放入Promise.all与其他异步任务并行编排。四、源码级原理一行代码背后的并发控制forEachAsync的实现非常精简见 src/array/forEachAsync.tsexport async function forEachAsyncT( array: readonly T[], callback: (item: T, index: number, array: readonly T[]) Promisevoid, options?: ForEachAsyncOptions ): Promisevoid { if (options?.concurrency ! null) { callback limitAsync(callback, options.concurrency); } await Promise.all(array.map(callback)); }核心逻辑只有两步若不指定concurrency直接Promise.all(array.map(callback))所有回调同时启动等价于完全并发若指定concurrency先用limitAsync把回调包装成限流版本再交给Promise.all调度。4.1 limitAsync用信号量包装回调limitAsync位于 src/promise/limitAsync.ts它创建一个计数信号量Semaphore并返回包装后的函数export function limitAsyncF extends (...args: any[]) Promiseany(callback: F, concurrency: number): F { const semaphore new Semaphore(concurrency); return async function (this: ThisTypeF, ...args: ParametersF): PromiseReturnTypeF { try { await semaphore.acquire(); return await callback.apply(this, args); } finally { semaphore.release(); } } as F; }关键设计点release()被放在finally中即使回调抛出异常信号量也一定会被归还不会因单个任务失败而卡死整个并发池。注意limitAsync也被 src/array/index.ts 单独导出你可以直接复用它来限制任意异步函数的并发数。4.2 SemaphoreFIFO 公平调度信号量实现位于 src/promise/semaphore.ts维护两个核心状态available当前可用许可数量deferredTasks等待队列采用 FIFO先入先出顺序保证公平性。acquire()的逻辑若有可用许可则直接扣减并立即返回否则把当前任务的 resolve 推入等待队列挂起。release()的逻辑若等待队列非空则取出队首任务并唤醒它把许可接力给下一位否则在不超过capacity的前提下归还许可。这套机制保证了同一时刻最多只有concurrency个回调在真正执行其余回调按调用顺序排队等待。五、行为边界与测试验证仓库中的测试forEachAsync.spec.ts覆盖了五个关键行为可作为使用时的行为契约参考测试场景验证结论对每个元素异步执行回调回调被调用arr.length次参数顺序为(item, index, array)空数组回调调用次数为 0函数正常 resolve任一回调抛出异常forEachAsync会 reject错误传播指定concurrency: 2实测最大并发数maxRunning 2且所有元素都被处理不指定concurrency10 个元素的实测最大并发数为 10即完全并发其中两个边界值得特别注意空数组安全forEachAsync([], callback)不会调用任何回调直接 resolve可放心对可能为空的数组调用错误快速失败fail-fast由于底层使用Promise.all一旦某个回调 rejectforEachAsync立即以该错误 reject符合任一失败即整体失败的语义。如果你的场景需要单个失败不中断整体应先在回调内部自行捕获异常。六、典型实战场景结合concurrency的取值forEachAsync可以覆盖从串行到完全并发的完整谱系数据库批量更新concurrency: 5左右避免同时建立大量数据库连接导致连接池耗尽文件批量上传/下载concurrency: 1或较小值保护带宽与目标服务器外部 API 调用设置合理的并发上限遵守第三方服务的速率限制rate limit日志批量写入无需返回值遍历日志数组逐条落盘可用较高并发提升吞吐清理任务、通知推送不需要收集结果、只需全部做完再继续的批处理任务。七、相关资源中文文档主体来源docs/ja/reference/array/forEachAsync.md英文版见 docs/reference/array/forEachAsync.md源码实现src/array/forEachAsync.ts单元测试src/array/forEachAsync.spec.ts并发控制基础src/promise/limitAsync.ts、src/promise/semaphore.ts模块导出入口src/array/index.ts同类异步工具中filterAsync 支持用异步谓词过滤数组并同样支持concurrency限制如果你需要逆序遍历数组可参考同步版本的 forEachRight。需要说明的是forEachAsync的设计目标是执行副作用并等待完成它不收集回调的返回值——若你需要对结果做变换或收集应优先选择mapAsync、filterAsync等返回数据集的工具。【免费下载链接】es-toolkitA modern JavaScript utility library thats 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考