JS 代码技巧 vol.4 --- 10 个异步并发控制,Promise.all 这帮兄弟的踩坑实录
小不的代码技巧系列第 4 期
主题:异步并发控制
这期是真的"踩坑里淌过来的血"------并发这事,写对是性能,写错是玄学 😎
哈喽哇!我是小不 ,不简说的不~
作为一个在代码界"翻车"无数次的选手,我算是看明白了:坑这东西吧,要么不踩,踩就踩大的😂
今天这期,主打一个 异步并发控制------不整虚的,全是实战里哭出来的教训。看不看随你~反正翻车实录又不收钱
Ps:文末有惊喜(不是广告!)
1. Promise.all:一个失败全盘皆输
经典场景:批量上传 10 个文件,3 个失败,剩下 7 个其实成功的------但用户啥都看不到。
js
// ❌ 一个 reject,全部 reject
const results = await Promise.all(files.map(upload));
// 只要 upload 中任何一个 throw,外层 await 直接抛错
正解------用 allSettled:
js
const results = await Promise.allSettled(files.map(upload));
const success = results
.filter((r) => r.status === "fulfilled")
.map((r) => r.value);
const failed = results
.filter((r) => r.status === "rejected")
.map((r) => r.reason);
坑在哪 :allSettled 不抛错,所有结果都要自己从 {status, value|reason} 里拆。批量场景首选它,别一上来就 Promise.all。
2. Promise.race:超时控制的灵魂
经典场景:fetch 偶尔挂死(网络问题),要给个兜底超时。
js
function fetchWithTimeout(url, ms = 5000) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("timeout")), ms),
),
]);
}
坑在哪 :fetch 那个 Promise 还是会继续在飞------超时后内存里还挂着,连接也没真断。想真取消得用 AbortController(见 tip 9)。
3. Promise.any:找最快成功的那个
js
const fastest = await Promise.any([fetchCDN1(), fetchCDN2(), fetchCDN3()]);
// 三个里谁先成功用谁的结果
ES2021 加的,比手写 race + try/catch 简洁不少。
坑在哪 :全部 reject 时抛 AggregateError,错误数组在 .errors 上,要专门 catch,否则报错信息一脸懵。
4. forEach 不支持 await 顺序
js
// ❌ 反例:典型踩坑
files.forEach(async (file) => {
await upload(file);
});
console.log("上传完成"); // ❌ 立刻打印,文件还在上传中
正解------for...of 串行:
js
for (const file of files) {
await upload(file);
}
console.log("上传完成"); // ✓ 真·上传完才打印
坑在哪 :forEach 把 async 回调当 fire-and-forget,return 的 Promise 全被丢了。需要并行的就 Promise.all,需要串行的用 for...of,别用 forEach + async。
5. 微观:串行 vs 并行的性能差距
10 个 1 秒的请求:
js
// 串行:10 秒
for (const id of ids) await fetchUser(id);
// 并行:≈1 秒(瓶颈是网络)
const users = await Promise.all(ids.map(fetchUser));
坑在哪:并行不是越多越好,浏览器对同一域名并发有限(一般 6 个),多了排队。Node 服务端也别瞎并行,数据库连接池小的话直接打爆。
6. 手写并发池:限流核心
js
async function pool(tasks, limit = 3) {
const results = [];
const executing = new Set();
for (const task of tasks) {
const p = Promise.resolve().then(() => task());
results.push(p);
executing.add(p);
const clean = () => executing.delete(p);
p.then(clean, clean);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
用法:
js
const tasks = urls.map((url) => () => fetch(url));
await pool(tasks, 3); // 同时最多 3 个
坑在哪 :手写池子容易死锁(比如 executing 没清理干净)。生产用 p-limit / async-pool 等库省心,自己写也就 30 行,但每个边界条件都得测一遍。
7. async/await 错误传播:for 循环里一个挂全挂
js
// ❌ 反例:上传 5 个文件,第二个失败后面 3 个直接不跑
for (const file of files) {
await upload(file);
}
正解------每个独立 try/catch:
js
for (const file of files) {
try {
await upload(file);
success.push(file);
} catch (e) {
failed.push({ file, error: e });
}
}
坑在哪 :业务上是"部分失败容忍"还是"全成功才行"------前者写后者就丢数据,后者写前者又不够"宽容"。想清楚再写!想清楚再写!想清楚再写! ⚠️
8. 重试 + 指数退避
js
async function retry(fn, retries = 3, base = 100) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (e) {
if (i === retries - 1) throw e;
// 100, 200, 400, 800... 加点随机防雪崩
const delay = base * 2 ** i + Math.random() * 100;
await new Promise((r) => setTimeout(r, delay));
}
}
}
坑在哪 :重试幂等性!非幂等的请求(POST)重试会重复下单 ------必须带 idempotency key。必须带!必须带!必须带! ⚠️ 加点 jitter 随机延迟是防雪崩的关键。
9. 取消 fetch:AbortController 现代做法
js
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(url, { signal: controller.signal });
clearTimeout(timer);
// 用数据
} catch (e) {
if (e.name === "AbortError") console.log("主动取消");
else throw e;
}
坑在哪 :老浏览器不支持 AbortController(虽然 2026 年基本普及),要做 polyfill。Node 18+ 原生支持 signal。配合 tip 2 的超时模式一起用,才是真·超时取消。 【⚠️】请求就像泼出去的水,不能回撤。 取消后,请求会继续,但是会被忽略。
10. 顺序控制的"心智模型":并行 / 串行 / 池化
一句话速记:
- 全并行 :
Promise.all(tasks.map(t => t()))------ 简单暴力,可能压垮服务 - 全串行 :
for (const t of tasks) await t()------ 慢,但顺序可控 - 池化限流 :
pool(tasks, n)------ 实战首选,平衡性能和资源
坑在哪 :业务场景没想清楚就拍脑袋选------比如写日志收集用全并行直接挂服务、用户操作链用全串行响应慢到被骂。用错模式背锅的是自己 😢
📦 收个尾
这 10 个技巧浓缩一下:
- 最高频踩坑 :
Promise.all一挂全挂、forEach不支持 await、try/catch跨 setTimeout - 最值得收藏 :手写并发池、
AbortController真取消、Promise.allSettled - 核心思路 :并行 ≠ 全
Promise.all,按场景选模式
学到了就是赚到了,犹豫徘徊等于白来~
写到最后
想要啥技巧?评论区甩个题目过来~
- 你刚踩的坑
- 项目里反复写的代码
- 想搞清楚但一直懒得查的 API
小不看到...不一定回 😂 毕竟代码里翻车太多,腾不出手~
Ps:三连随缘,催更的会被打 😂