前言
从 Node 8 到浏览器 ES2017,Promise 早已是前端/全栈的“空气”——无处不在,却又常常让人踩坑:
“并发开多大?”“请求怎么取消?”“失败要不要重试?”“为什么又回调地狱了?”
这篇文章把我过去五年在业务中反复验证的 15 条实用技巧 一次性梳理给你。
读完你可以:
- 10 秒内写出“并发池 + 超时 + 重试”全套代码;
- 彻底告别
then金字塔,用同步思维写异步; - 精准定位内存泄漏,单元测试不再“真等 7 秒”。
目录
-
回调一键变 Promise:零成本 promisify
-
并发限速:p-limit 让后端不崩溃
-
请求竞速与取消:AbortController
-
超时熔断:Promise.race 的 N 种玩法
-
优雅重试:指数退避算法
-
无论成败都清理:finally 的正确姿势
-
顺序 ≠ Promise.all:for…of 的秘密
-
拍平回调地狱:async/await 语法糖
-
单元测试 fake timers:毫秒跑完 7 秒重试
-
微任务与宏任务:then 和 setTimeout 谁先?
-
allSettled:批量上报不再 try/catch 满天飞
-
可取消 Promise:大文件上传用户点“停”
-
链式共享中间值:避免地狱传参
-
调试黑科技:Promise 钩子
-
并发池到底多大?经验值与压测
1. 回调一键变 Promise:零成本 promisify
场景
老库/Node 核心模块还是 (err, value) => {} 风格。
代码
import { promisify } from 'node:util';
import fs from 'node:fs';
const readFile = promisify(fs.readFile);
const buf = await readFile('package.json'); // 直接 await
要点
-
不要自己包
new Promise,Node 已内置; -
非标准回调先“包装”成标准形式再
promisify。
2. 并发限速:p-limit 让后端不崩溃
场景
1000 个接口请求,并发 5,防止把下游打挂。
代码
npm i p-limit
import pLimit from 'p-limit';
const limit = pLimit(5);
const urlList = Array.from({ length: 1000 }, (_, i => `https://api.x.com/${i}`);
const data = await Promise.all(
urlList.map(u => limit(() => fetch(u).then(r => r.json())))
);
要点
-
一行包一层
limit(() => ...),代码量≈0; -
想带进度条用
p-map,支持onProgress回调。
3. 请求竞速与取消:AbortController
场景
用户狂点搜索框,只保留最后一次请求。
代码
let ctrl = new AbortController();
async function search(keyword) {
ctrl.abort(); // 取消上一次
ctrl = new AbortController(); // 新建
const res = await fetch(`/api/search?q=${keyword}`, { signal: ctrl.signal });
return res.json();
}
要点
-
fetch原生支持,无需额外库; -
捕获
AbortError静默处理,UI 不抖。
4. 超时熔断:Promise.race 的 N 种玩法
场景
接口 5 秒没回直接抛错,避免白转圈。
代码
const timeout = (ms, promise) =>
Promise.race([
promise,
new Promise((_, rej) =>
setTimeout(() => rej(new Error(`Timeout after ${ms}ms`)), ms)
)
]);
await timeout(5000, fetch('/api/slow'));
要点
-
通用“套壳”,任意 Promise 都能加;
-
finally里清掉定时器,极端场景防内存泄漏。
5. 优雅重试:指数退避算法
场景
网络抖动,自动重试 3 次,间隔 1s→2s→4s。
代码
async function retry(fn, { times = 3, delay = 1000 } = {}) {
for (let i = 0; i < times; i++) {
try {
return await fn();
} catch (e) {
if (i === times - 1) throw e;
await new Promise(r => setTimeout(r, delay * 2 ** i));
}
}
}
const data = await retry(() => fetch('/api/flaky'));
要点
-
指数退避
2**i是云厂商推荐策略; -
fn写成函数调用,每次都“新建”请求。
6. 无论成败都清理:finally 的正确姿势
场景
loading 动画、释放资源。
代码
spinner.show();
await fetch('/api')
.finally(() => spinner.hide());
要点
-
finally拿不到结果/错误,仅做清理; -
链式顺序无影响,永远最后执行。
7. 顺序 ≠ Promise.all:for…of 的秘密
场景
批量写入,后端要求串行。
错误
await Promise.all(list.map(item => create(item))); // 并发
正确
for (const item of list) {
await create(item);
}
要点
-
map+Promise.all是并发; -
for…of+await才是串行,别搞反。
8. 拍平回调地狱:async/await 语法糖
代码对比
// 回调地狱
getUser(uid, (err, user) => {
if (err) return cb(err);
getOrder(user.id, (err, order) => {
if (err) return cb(err);
getGoods(order.gid, (err, goods) => {
if (err) return cb(err);
cb(null, goods);
});
});
});
// 拍平后
const user = await getUserAsync(uid);
const order = await getOrderAsync(user.id);
const goods = await getGoodsAsync(order.gid);
要点
-
统一
promisify后,异常用try/catch一网打尽; -
想再并发,可回退到
Promise.all。
9. 单元测试 fake timers:毫秒跑完 7 秒重试
代码
jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');
const pending = retry(fn, { times: 3, delay: 1000 });
for (let i = 0; i < 3; i++) {
jest.runAllTimers(); // 一口气跑完
await Promise.resolve(); // 微任务 flush
}
要点
-
不用真等 7 秒;
-
flush-promises一行搞定await Promise.resolve()。
10. 微任务与宏任务:then 和 setTimeout 谁先?
代码
console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
console.log(4);
// 输出:1 4 3 2
要点
-
then属微任务,比宏任务setTimeout先执行; -
动画、日志、埋点别指望
setTimeout 0一定比then快。
11. allSettled:批量上报不再 try/catch 满天飞
代码
const arr = await Promise.allSettled(
urls.map(u => fetch(u))
);
arr.forEach(({ status, value, reason }) =>
status === 'fulfilled'
? console.log('成功', value)
: console.log('失败', reason)
);
要点
-
allSettled永不抛错,适合批量上报; -
老浏览器用
core-jspolyfill。
12. 可取消 Promise:大文件上传用户点“停”
代码
npm i p-cancelable
import PCancelable from 'p-cancelable';
const job = new PCancelable((resolve, reject, onCancel) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/big');
xhr.send(file);
onCancel(() => xhr.abort());
xhr.onload = () => resolve(xhr.responseText);
});
button.onclick = () => job.cancel();
const txt = await job;
要点
-
取消 ≠ 回滚,已上传需后端配合校验;
-
fetch一旦abort()无法复活,记得新建请求对象。
13. 链式共享中间值:避免地狱传参
代码
const A = await getA();
const [B, C] = await Promise.all([getB(A), getC(A)]);
要点
-
async函数内变量直接共享,不必像then链层层传参。
14. 调试黑科技:Promise 钩子
代码
import { setPromiseHooks } from 'node:async_hooks';
setPromiseHooks({
init(promise) {
console.log('创建', promise);
},
resolve(promise) {
console.log('完成', promise);
}
});
要点
-
性能分析、内存泄漏定位时打开;
-
生产环境记得关,避免日志爆炸。
15. 并发池到底多大?经验值与压测
| 场景 | 推荐并发 |
| | |
| 浏览器同一域名 | 6~10 |
| Node CPU 密集 | os.cpus().length * 2 |
| Node IO 密集 | 可再翻倍,务必压测 |
速查表(Cheatsheet)
| 需求 | 一句话方案 |
| | |
| callback → Promise | promisify / new Promise |
| 并发上限 | p-limit |
| 超时 | Promise.race + setTimeout |
| 重试 | for 循环 + 指数退避 |
| 取消 | AbortController / PCancelable |
| 顺序 | for…of + await |
| 全部完成不抛错 | Promise.allSettled |
| 不管成败都清理 | .finally() |
结语
掌握这 15 招,你将获得:
- 代码量 −50%、异常堆栈 −80%;
- 线上超时/重试/取消 零配置 可用;
- 测试用例 毫秒级 跑完,CI 直接起飞。
把本文点赞收藏,下次写异步不用谷歌,复制即可。
祝你编码愉快,Promise 永不 pending!