异步编程实践
本讲系统讲解异步编程在工程实践中的常见模式,包括并发控制、重试机制、超时处理、缓存去重、异步队列、事件驱动、轮询、取消操作与错误处理,帮助你写出健壮、可维护的异步代码。
一、并发控制
限制并发数量
当需要同时执行大量异步任务(如批量下载图片、批量请求接口)时,应限制并发数量,避免资源耗尽或触发服务端限流。
javascript
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()));批量处理(分批执行)
将大量任务拆分为多批,逐批执行,避免单次触发过多异步操作:
javascript
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 | 固定并发上限 | 并发数较多 |
| 分批执行 | 按批次串行 | 需要分批处理 |
二、重试机制
基本重试
对不稳定接口增加重试,提升成功率:
javascript
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));
}
}
}指数退避重试
逐次递增重试间隔,避免对服务端造成冲击:
javascript
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 实现请求超时控制:
javascript
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); // 请求超时 或 真正的请求错误
}超时配置最佳实践
- 超时后清理:超时后应尝试取消底层请求(见下文 AbortController),避免资源泄漏。
- 区分错误类型:超时错误与请求错误应能区分,便于分类处理。
- 合理设置超时:依据接口耗时特性设置,不宜过短或过长。
四、缓存与请求去重
简单缓存
缓存已完成的 Promise,避免重复请求:
javascript
const cache = new Map();
function cachedFetch(url) {
if (cache.has(url)) {
return cache.get(url); // 命中缓存,直接返回
}
const promise = fetch(url)
.then((res) => res.json())
.finally(() => {
// 缓存失败时删除,便于下次重试
cache.delete(url);
});
cache.set(url, promise);
return promise;
}请求去重
对于相同参数的并发请求,合并为一次真实请求:
javascript
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);缓存策略对比
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 永久缓存 | 结果不变化 | 静态数据 |
| 过期缓存 | 一段时间后失效 | 频繁读取 |
| 请求去重 | 合并并发请求 | 热点请求 |
五、异步队列
基本任务队列
实现一个带并发控制的任务队列:
javascript
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);
});六、事件驱动
异步事件发射器
基于发布-订阅模式,解耦异步流程中的多个模块:
javascript
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));七、轮询
基本轮询
定期查询服务器状态,直到满足条件:
javascript
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 API
使用 AbortController 取消 fetch 请求:
javascript
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);
}
}自定义取消机制
通过标志位实现手动取消:
javascript
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:
javascript
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 });错误分类处理
根据错误类型采取不同策略(重试、降级、提示用户等):
javascript
class ApiError extends Error {
constructor(message, code) {
super(message);
this.code = code;
}
}
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并行发起。 - 避免在 forEach 中 await:改用
for...of或Promise.all。 - 避免无限递归微任务:会饿死宏任务,导致页面卡死。
- 及时清理定时器与监听器:防止内存泄漏。
内存优化
javascript
// 及时移除事件监听器
const handler = () => { /* ... */ };
element.addEventListener('scroll', handler);
// 组件卸载时移除
element.removeEventListener('scroll', handler);
// 清理定时器
const timer = setInterval(() => { /* ... */ }, 1000);
// 不再需要时
clearInterval(timer);十一、常见问题
Q1: 如何处理多个异步操作的错误?
- 互不依赖的并行操作:
Promise.all(任一失败即失败)或Promise.allSettled(统计全部结果)。 - 相互依赖的串行操作:
try/catch包裹,配合重试机制。 - 需要区分错误:对错误分类处理。
Q2: 如何实现请求的串行执行?
使用 for...of 逐个 await,或通过 reduce 串联 Promise:
javascript
async function sequential(tasks) {
const results = [];
for (const task of tasks) {
results.push(await task());
}
return results;
}Q3: 如何检测 Promise 是否已 settled?
没有直接 API,可通过 Promise.race 包装或自行维护状态标志:
javascript
function createSettledFlag() {
let settled = false;
const promise = new Promise((resolve, reject) => {
// ...
});
promise.finally(() => {
settled = true;
});
return { isSettled: () => settled, promise };
}Q4: 如何优雅地处理异步初始化?
在模块加载时初始化,使用顶层 await(ES2022)或将初始化结果缓存:
javascript
let configPromise;
export function getConfig() {
if (!configPromise) {
configPromise = loadConfig(); // 只初始化一次
}
return configPromise;
}十二、总结
异步编程的工程实践核心原则:
- 控制并发:使用
mapLimit、批量处理避免资源耗尽。 - 提升可靠性:为不稳定操作增加重试(指数退避)与超时处理。
- 避免重复:使用缓存与请求去重。
- 可管理性:使用异步队列、事件驱动组织复杂流程。
- 可取消:使用
AbortController或标志位实现取消。 - 健壮性:统一错误处理,分类应对不同错误。
选择合适的异步模式
| 场景 | 推荐方案 |
|---|---|
| 多个互不依赖的请求 | Promise.all |
| 大量任务需限制并发 | mapLimit / 任务队列 |
| 不稳定接口 | 重试 + 指数退避 |
| 需要控制耗时 | withTimeout 超时处理 |
| 避免重复请求 | 缓存 + 请求去重 |
| 需要中途取消 | AbortController |