在真实的业务场景中,异步操作远不止“发一个请求,等结果”这么简单。很多时候我们需要同时发起多个请求以提高效率,但又不能无限制地并发;网络可能抖动,请求可能失败,需要安全的自动重试;某些操作拖得太久,必须设置超时结束;还有多个异步操作的结果可能交错到达,导致界面显示与用户操作不一致。这一节,我们就来解决这些异步编程中的“进阶控制”问题。
12.4.1 并发限流
假设需要调用一个分页接口抓取 100 页数据,如果同时发出 100 个请求,不仅会对服务端造成冲击,浏览器也可能因为连接数限制导致部分请求阻塞,整体效率并不一定高。更好的做法是限制同时请求的数量,例如同时只允许 3 个请求在执行,一个完成后再启动下一个。
通用限流函数实现
async function asyncPool(limit, tasks) {
const results = [];
const executing = new Set();
for (const task of tasks) {
const promise = Promise.resolve().then(() => task());
results.push(promise);
executing.add(promise);
const clean = () => executing.delete(promise);
promise.then(clean, clean);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
用法示例:同时最多 3 个请求
const urls = ['/api/data?page=1', '/api/data?page=2', /* ... 100个 */];
const tasks = urls.map(url => () => fetch(url).then(res => res.json()));
asyncPool(3, tasks).then(allResults => {
console.log('所有请求完成', allResults);
});
核心思路:维护一个执行中的 Promise 集合,当集合大小达到上限时,用 Promise.race 等待任一完成,腾出空位后再继续添加新任务。实际开发中也可以直接使用成熟的工具库,如 p-limit。
12.4.2 异步重试
网络请求可能因为临时故障而失败,直接在代码里 try/catch 然后放弃未免太脆弱。一个健壮的系统应当具备自动重试机制,并配合适当的延迟(避免“惊群效应”)。
带重试的 fetch 封装
async function fetchWithRetry(url, options = {}, retries = 3, backoff = 300) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response;
} catch (err) {
if (i === retries - 1) throw err; // 最后一次仍失败,抛出错误
// 指数退避延迟:第1次等300ms,第2次等600ms...
await new Promise(resolve => setTimeout(resolve, backoff * (i + 1)));
}
}
}
用法:
const data = await fetchWithRetry('/api/unreliable-endpoint')
.then(res => res.json());
重试策略可以根据业务需求调整:固定延迟、指数退避、加上随机抖动(防止大量客户端同时重试造成服务端压力)。
12.4.3 超时处理
有些异步操作没有内置的超时机制(例如 fetch 自身支持 AbortController 但需要手动封装),或者你想要强制一个 Promise 必须在规定时间内完成,否则按失败处理。
为任何 Promise 添加超时
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('操作超时')), ms)
)
]);
}
使用 AbortController 取消底层 fetch(推荐)
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { ...options, signal: controller.signal });
return response;
} finally {
clearTimeout(timer);
}
}
这种方式不仅在外层抛出了超时错误,还真正中止了网络请求,释放了连接资源。
12.4.4 竞态处理
在用户快速切换页面、连续点击按钮或输入搜索关键词时,多个异步操作可能“后发先至”,导致最后渲染的结果与最后一次操作意图不匹配。例如:用户快速输入“abc”,依次发出 3 个搜索请求,但第二个请求(搜索“ab”)的网络返回较慢,结果在第三个请求(“abc”)之后到达,搜索结果就会被错误地显示为“ab”的结果。这就是典型的异步竞态问题。
解决思路:丢弃“过时”的响应
方法一:使用标志位
let requestId = 0;
async function search(keyword) {
const currentId = ++requestId;
const results = await fetch(`/api/search?q=${keyword}`).then(res => res.json());
if (currentId === requestId) {
// 只有最新一次的请求结果才会更新 UI
updateUI(results);
}
}
方法二:利用 AbortController 取消前一个请求
let lastController = null;
async function search(keyword) {
if (lastController) {
lastController.abort(); // 取消上一个请求
}
const controller = new AbortController();
lastController = controller;
try {
const results = await fetch(`/api/search?q=${keyword}`, {
signal: controller.signal
}).then(res => res.json());
updateUI(results);
} catch (err) {
if (err.name === 'AbortError') {
// 请求被取消,这是预期行为,无需处理
console.log('请求被取消');
} else {
console.error(err);
}
}
}
现代前端框架通常有更上层的方法来处理竞态。例如 React 的 useEffect 中清理副作用,或用 useRef 存储最新请求标识;Vue 的 watchEffect 和 onCleanup 也能配合取消。但原理都离不开以上两种模式。
12.4.5 综合案例:分页数据全量获取
下面是一个结合限流、重试、超时的实用场景:将一个 100 页的数据全部下载下来,每页最多并发 5 个请求,每个请求超时 10 秒,失败自动重试 2 次。
async function fetchAllPages(baseUrl, totalPages) {
const tasks = Array.from({ length: totalPages }, (_, i) => async () => {
const url = `${baseUrl}?page=${i + 1}`;
return fetchWithTimeoutAndRetry(url);
});
return asyncPool(5, tasks);
}
async function fetchWithTimeoutAndRetry(url, retries = 2, timeout = 10000) {
for (let i = 0; i <= retries; i++) {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timer);
if (!response.ok) throw new Error('Status not OK');
return response.json();
} catch (err) {
if (i === retries) throw err;
await new Promise(r => setTimeout(r, 500 * (i + 1)));
}
}
}
这样的组合策略能显著提升大批量异步任务处理的鲁棒性与效率,是真实工程中不可或缺的能力。
掌握了异步并发控制,再结合下一节的错误捕获与全局监控,你就有了一套完整的异步编程“防崩”体系。