本讲系统讲解异步编程在工程实践中的常见模式,包括并发控制、重试机制、超时处理、缓存去重、异步队列、事件驱动、轮询、取消操作与错误处理,帮助你写出健壮、可维护的异步代码。
当需要同时执行大量异步任务(如批量下载图片、批量请求接口)时,应限制并发数量,避免资源耗尽或触发服务端限流。
async function mapLimit(tasks, limit, fn) {
const results = [];
let index = 0;
async function worker() {
while (index < tasks.length) {
const current = index++;
results[current] = await fn(tasks[current]);
}
}
const workers = Array.from({ length: limit }, () => worker());
await Promise.all(workers);
return results;
}
// 使用:最多同时 3 个并发请求
const results = await mapLimit(urls, 3, (url) => fetch(url).then((r) => r.json()));将大量任务拆分为多批,逐批执行,避免单次触发过多异步操作:
async function batchProcess(items, batchSize, fn) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map((item) => fn(item)));
results.push(...batchResults);
}
return results;
}| 方式 | 特点 | 适用场景 |
|---|---|---|
Promise.all | 一次性全部并发 | 并发数可控 |
mapLimit | 固定并发上限 | 并发数较多 |
| 分批执行 | 按批次串行 | 需要分批处理 |
对不稳定接口增加重试,提升成功率:
async function fetchWithRetry(fn, { retries = 3, delay = 1000 } = {}) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i === retries - 1) throw error;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}逐次递增重试间隔,避免对服务端造成冲击:
async function fetchWithBackoff(fn, { retries = 3, baseDelay = 500, factor = 2 } = {}) {
let delay = baseDelay;
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i === retries - 1) throw error;
await new Promise((resolve) => setTimeout(resolve, delay));
delay *= factor; // 指数递增:500 → 1000 → 2000
}
}
}| 策略 | 间隔 | 适用场景 |
|---|---|---|
| 固定重试 | 恒定 | 临时抖动 |
| 指数退避 | 递增 | 服务过载 |
| 抖动退避 | 递增 + 随机 | 避免同时重试 |
使用 Promise.race 实现请求超时控制:
function withTimeout(promise, ms, message = '请求超时') {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(message)), ms)
),
]);
}
// 使用
try {
const data = await withTimeout(fetchData(), 5000);
} catch (error) {
console.error(error.message); // 请求超时 或 真正的请求错误
}缓存已完成的 Promise,避免重复请求:
const cache = new Map();
function cachedFetch(url) {
if (cache.has(url)) {
return cache.get(url); // 命中缓存,直接返回
}
const promise = fetch(url)
.then((res) => res.json())
.catch((error) => {
// 失败时删除缓存,便于下次重试;成功结果保留在缓存中
cache.delete(url);
throw error;
});
cache.set(url, promise);
return promise;
}对于相同参数的并发请求,合并为一次真实请求:
const inFlight = new Map();
function dedupe(fn, getKey) {
return function (...args) {
const key = getKey(...args);
if (inFlight.has(key)) {
return inFlight.get(key); // 复用正在进行的请求
}
const promise = fn(...args).finally(() => inFlight.delete(key));
inFlight.set(key, promise);
return promise;
};
}
const fetchUser = dedupe((id) => api.getUser(id), (id) => id);| 策略 | 说明 | 适用场景 |
|---|---|---|
| 永久缓存 | 结果不变化 | 静态数据 |
| 过期缓存 | 一段时间后失效 | 频繁读取 |
| 请求去重 | 合并并发请求 | 热点请求 |
实现一个带并发控制的任务队列:
class TaskQueue {
constructor(concurrency = 1) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this._run();
});
}
_run() {
while (this.running < this.concurrency && this.queue.length) {
const { task, resolve, reject } = this.queue.shift();
this.running++;
task()
.then(resolve, reject)
.finally(() => {
this.running--;
this._run();
});
}
}
}
// 使用
const queue = new TaskQueue(2);
[1, 2, 3, 4].forEach((i) => {
queue.add(() => fetchTask(i)).then(console.log);
});基于发布-订阅模式,解耦异步流程中的多个模块:
class AsyncEventEmitter {
constructor() {
this.handlers = new Map();
}
on(event, handler) {
if (!this.handlers.has(event)) this.handlers.set(event, []);
this.handlers.get(event).push(handler);
}
emit(event, ...args) {
const handlers = this.handlers.get(event) || [];
handlers.forEach((handler) => handler(...args));
}
off(event, handler) {
const handlers = this.handlers.get(event) || [];
const index = handlers.indexOf(handler);
if (index > -1) handlers.splice(index, 1);
}
}
// 使用
const emitter = new AsyncEventEmitter();
emitter.on('dataLoaded', (data) => console.log('数据加载完成:', data));
loadData().then((data) => emitter.emit('dataLoaded', data));定期查询服务器状态,直到满足条件:
function poll(fn, { interval = 1000, timeout = 30000 } = {}) {
return new Promise((resolve, reject) => {
const start = Date.now();
async function check() {
try {
const result = await fn();
if (result.done) {
resolve(result);
return;
}
if (Date.now() - start > timeout) {
reject(new Error('轮询超时'));
return;
}
setTimeout(check, interval);
} catch (error) {
reject(error);
}
}
check();
});
}
// 使用:轮询任务状态
poll(() => getTaskStatus(taskId), { interval: 2000, timeout: 60000 })
.then((result) => console.log('任务完成:', result));使用 AbortController 取消 fetch 请求:
const controller = new AbortController();
const signal = controller.signal;
// 发起请求
const promise = fetch('/api/large-data', { signal });
// 5 秒后取消
setTimeout(() => controller.abort(), 5000);
try {
const data = await promise;
} catch (error) {
if (error.name === 'AbortError') {
console.log('请求已取消');
} else {
console.error('请求失败:', error);
}
}通过标志位实现手动取消:
function createCancellable(fn) {
let cancelled = false;
async function run(...args) {
const result = await fn(...args);
if (cancelled) throw new Error('Operation cancelled');
return result;
}
run.cancel = () => {
cancelled = true;
};
return run;
}
const task = createCancellable(loadData);
const promise = task();
// task.cancel(); // 取消封装统一的错误处理函数,避免每个异步调用处重复 try/catch:
async function safeCall(fn, fallback = null) {
try {
return await fn();
} catch (error) {
console.error('调用失败:', error);
return fallback;
}
}
// 使用
const user = await safeCall(() => fetchUser(id), { id: -1 });根据错误类型采取不同策略(重试、降级、提示用户等):
class ApiError extends Error {
constructor(message, code) {
super(message);
this.code = code;
}
}
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function handleRequest(fn) {
try {
return await fn();
} catch (error) {
if (error.code === 401) {
// 未授权:跳转登录
redirectToLogin();
} else if (error.code === 429) {
// 限流:退避重试
await delay(2000);
return handleRequest(fn);
} else {
// 其他:提示用户
showError(error.message);
}
throw error;
}
}Promise.all 并行发起。for...of 或 Promise.all。// 及时移除事件监听器
const handler = () => { /* ... */ };
element.addEventListener('scroll', handler);
// 组件卸载时移除
element.removeEventListener('scroll', handler);
// 清理定时器
const timer = setInterval(() => { /* ... */ }, 1000);
// 不再需要时
clearInterval(timer);Promise.all(任一失败即失败)或 Promise.allSettled(统计全部结果)。try/catch 包裹,配合重试机制。使用 for...of 逐个 await,或通过 reduce 串联 Promise:
async function sequential(tasks) {
const results = [];
for (const task of tasks) {
results.push(await task());
}
return results;
}没有直接 API,可通过 Promise.race 包装或自行维护状态标志:
function createSettledFlag() {
let settled = false;
const promise = new Promise((resolve, reject) => {
// ...
});
promise.finally(() => {
settled = true;
});
return { isSettled: () => settled, promise };
}在模块加载时初始化,使用顶层 await(ES2022)或将初始化结果缓存:
let configPromise;
export function getConfig() {
if (!configPromise) {
configPromise = loadConfig(); // 只初始化一次
}
return configPromise;
}异步编程的工程实践核心原则:
mapLimit、批量处理避免资源耗尽。AbortController 或标志位实现取消。| 场景 | 推荐方案 |
|---|---|
| 多个互不依赖的请求 | Promise.all |
| 大量任务需限制并发 | mapLimit / 任务队列 |
| 不稳定接口 | 重试 + 指数退避 |
| 需要控制耗时 | withTimeout 超时处理 |
| 避免重复请求 | 缓存 + 请求去重 |
| 需要中途取消 | AbortController |