Promise 进阶
并发控制、聚合与取消:
javascript
// 聚合
Promise.allSettled([fetch("/a"), fetch("/b")]).then((results) => {
// 每项都有 status 与 value/reason
});
// 竞速
Promise.any([fetch("/mirror1"), fetch("/mirror2")]).then((first) =>
console.log("first ok")
);
// 取消
const ac = new AbortController();
fetch("/data", { signal: ac.signal });
ac.abort(); // 取消请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
也可实现简单的并发池、重试等策略。
并发控制:并发池(Pool)
在网络或 I/O 密集任务中,合理限制并发度能避免拥塞、提升稳定性。
javascript
async function pool(tasks, limit = 4) {
const results = new Array(tasks.length);
let i = 0;
const workers = new Array(Math.min(limit, tasks.length))
.fill(0)
.map(async () => {
while (i < tasks.length) {
const cur = i++;
try {
results[cur] = await tasks[cur]();
} catch (e) {
results[cur] = Promise.reject(e);
}
}
});
await Promise.all(workers);
return results;
}
// 用法:
const urls = ["/a", "/b", "/c", "/d", "/e"];
pool(
urls.map((u) => () => fetch(u).then((r) => r.text())),
3
)
.then(console.log)
.catch(console.error);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
取消与超时
使用 AbortController
统一管理取消:
javascript
function fetchWithTimeout(
url,
{ timeout = 5000, signal: extSignal, ...opts } = {}
) {
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(new Error("timeout")), timeout);
const signals = [ac.signal];
if (extSignal)
extSignal.addEventListener("abort", () => ac.abort(extSignal.reason));
return fetch(url, { ...opts, signal: ac.signal }).finally(() =>
clearTimeout(timer)
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
结合 Promise.race
实现通用超时:
javascript
function withTimeout(p, ms) {
return Promise.race([
p,
new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms)),
]);
}
1
2
3
4
5
6
2
3
4
5
6
重试、退避与抖动(Retry/Backoff/Jitter)
javascript
async function retry(fn, { times = 3, base = 300, jitter = true } = {}) {
let attempt = 0,
lastErr;
while (attempt < times) {
try {
return await fn();
} catch (e) {
lastErr = e;
const backoff = base * 2 ** attempt;
const wait = jitter ? Math.random() * backoff : backoff;
await new Promise((r) => setTimeout(r, wait));
attempt++;
}
}
throw lastErr;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
策略:对网络暂时性错误(5xx、网络断开)重试,对 4xx(请求无效)不要重试。
批处理与节流:批量请求
javascript
function batch(items, size = 10) {
const res = [];
for (let i = 0; i < items.length; i += size)
res.push(items.slice(i, i + size));
return res;
}
for (const group of batch(ids, 20)) {
const resp = await Promise.all(group.map((id) => fetch(`/api/item/${id}`)));
// 处理 resp
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
错误分类与传递(Error Taxonomy)
统一错误形态有助于上层逻辑处理:
javascript
class HttpError extends Error {
constructor(status, body) {
super(`HTTP ${status}`);
this.status = status;
this.body = body;
}
}
class TimeoutError extends Error {
constructor(ms) {
super(`Timeout ${ms}ms`);
this.ms = ms;
}
}
class CancelError extends Error {
constructor() {
super("Cancelled");
this.isCancel = true;
}
}
async function request(url, opts) {
const res = await fetch(url, opts);
if (!res.ok) throw new HttpError(res.status, await res.text());
return res.json();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
对上层只抛出统一错误,便于集中处理与埋点。
清理与资源释放
在异步流程中,确保出现异常或取消时能完成清理:
javascript
async function useResource() {
const ac = new AbortController();
try {
const res = await fetch("/stream", { signal: ac.signal });
// ... 消费流
} finally {
ac.abort(); // 确保中止挂起的 I/O
}
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
常见陷阱
- 过度并发导致服务器限流或本地资源耗尽;
- 无超时/取消导致悬挂请求与资源泄漏;
- 重试未做幂等或未分类错误;
- 忘记在
then
中return
,导致链断裂; Promise.any
全部失败会抛AggregateError
,需专门捕获。
最佳实践
- 统一封装请求层:超时、取消、重试、错误分类在底层实现;
- 能并发就并发,但设置合理上限;
- 使用
AbortController
贯通取消; - 对重试策略施加最大重试次数与指数退避 + 抖动;
- 记录关键点日志与指标(成功率、平均时延、错误率)。
练习
- 实现一个有并发上限的下载器:输入 URL 列表,限制并发 5 并汇总结果;
- 将某接口封装为具备超时、取消与重试(指数退避)的
getWithPolicy(url)
; - 写一个任务队列:按 FIFO 顺序执行任务,支持暂停/恢复与取消。