返回博客
技术 2025年2月21日 5 分钟阅读 · 1080 字
JavaScript 异步编程演进:从回调地狱到 async/await 的完整历程
深入理解 JavaScript 异步编程的四个阶段:回调 → Promise → Generator → async/await,以及实际场景中的最佳实践。
#JavaScript
#异步编程
#Promise
#async/await
本文由 AI 辅助生成,经人工审核发布
回调地狱
JavaScript 最初处理异步的方式是回调函数:
// 回调地狱:层层嵌套
getUser(userId, function(err, user) {
if (err) return handleError(err);
getOrders(user.id, function(err, orders) {
if (err) return handleError(err);
getOrderItems(orders[0].id, function(err, items) {
if (err) return handleError(err);
getProductDetails(items[0].productId, function(err, product) {
if (err) return handleError(err);
console.log(product);
});
});
});
});
问题:
- 代码呈「金字塔」结构,难以阅读
- 错误处理分散在每个回调中
- 无法 return/throw——回调的返回值无人接收
Promise
ES6 引入 Promise,将嵌套改为链式调用:
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderItems(orders[0].id))
.then(items => getProductDetails(items[0].productId))
.then(product => console.log(product))
.catch(err => handleError(err));
Promise 的三种状态
pending(等待) → fulfilled(完成)
↘ rejected(拒绝)
状态一旦从 pending 变为 fulfilled 或 rejected,就不可逆转。
Promise.all vs Promise.race vs Promise.allSettled
// all:全部成功才成功,一个失败就失败
const [user, settings] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/settings').then(r => r.json())
]);
// race:第一个完成的结果(无论成功或失败)
const fastest = await Promise.race([
fetch('/api/primary'),
fetch('/api/fallback')
]);
// allSettled:等所有完成,无论成功或失败
const results = await Promise.allSettled([
fetch('/api/user'),
fetch('/api/settings'),
fetch('/api/notifications')
]);
// [{status: 'fulfilled', value: ...}, {status: 'rejected', reason: ...}, ...]
| 方法 | 用途 | 一个失败 |
|---|---|---|
all | 并行,全部需要 | 整体 reject |
race | 取最快 | 返回该结果 |
allSettled | 全部完成 | 不影响其他 |
async/await
ES2017 引入,让异步代码看起来像同步代码:
async function getProduct(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const items = await getOrderItems(orders[0].id);
const product = await getProductDetails(items[0].productId);
return product;
} catch (err) {
handleError(err);
}
}
错误处理
// 方式 1:try-catch
try {
const data = await fetchData();
} catch (err) {
console.error(err);
}
// 方式 2:.catch()(等价于 try-catch)
const data = await fetchData().catch(err => {
console.error(err);
return null;
});
// 方式 3:Go 风格的 helper
async function to(promise) {
try {
return [null, await promise];
} catch (err) {
return [err, null];
}
}
const [err, data] = await to(fetchData());
if (err) handleError(err);
并发控制
串行 vs 并行
// ❌ 串行:总时间 = A + B + C
const a = await fetchA(); // 1s
const b = await fetchB(); // 2s
const c = await fetchC(); // 3s
// 总计 6s
// ✅ 并行:总时间 = max(A, B, C)
const [a, b, c] = await Promise.all([
fetchA(),
fetchB(),
fetchC()
]);
// 总计 3s
限制并发数
当需要请求 100 个 API 但不能同时发 100 个请求时:
async function mapWithConcurrency(items, fn, limit) {
const results = new Array(items.length);
const executing = new Set();
for (let i = 0; i < items.length; i++) {
const promise = fn(items[i], i).then(result => {
results[i] = result;
executing.delete(promise);
});
results[i] = promise;
executing.add(promise);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// 最多同时 5 个请求
const results = await mapWithConcurrency(
urls,
url => fetch(url).then(r => r.json()),
5
);
取消异步操作
AbortController 可以取消 fetch 请求:
const controller = new AbortController();
// 5 秒后超时取消
setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('/api/slow-endpoint', {
signal: controller.signal
});
const data = await response.json();
} catch (err) {
if (err.name === 'AbortError') {
console.log('请求被取消');
}
}
// 主动取消
controller.abort();
React 中的取消模式
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
return () => controller.abort(); // 组件卸载时取消
}, []);
实际场景
重试机制
async function fetchWithRetry(url, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (err) {
if (i === retries - 1) throw err;
// 指数退避
await new Promise(r => setTimeout(r, delay * Math.pow(2, i)));
}
}
}
const data = await fetchWithRetry('/api/unstable-endpoint');
缓存 + 过期策略
const cache = new Map();
async function fetchWithCache(key, fetcher, ttl = 60000) {
const cached = cache.get(key);
if (cached && Date.now() - cached.time < ttl) {
return cached.value; // 返回缓存
}
const value = await fetcher();
cache.set(key, { value, time: Date.now() });
return value;
}
// stale-while-revalidate:先返回旧数据,后台更新
async function fetchSWR(key, fetcher, ttl = 60000) {
const cached = cache.get(key);
if (cached && Date.now() - cached.time < ttl) {
return cached.value;
}
if (cached) {
// 返回旧数据,同时后台更新
fetcher().then(value => cache.set(key, { value, time: Date.now() }));
return cached.value;
}
const value = await fetcher();
cache.set(key, { value, time: Date.now() });
return value;
}
顶层 await
ES2022 引入顶层 await,在模块顶层直接使用 await:
// config.js
export const config = await fetch('/api/config').then(r => r.json());
// app.js
import { config } from './config.js';
console.log(config.apiUrl); // 已就绪
无需包裹在 async 函数中,简化了模块级别的异步初始化。
总结
JavaScript 异步编程的演进方向是让异步代码越来越像同步代码:
- 回调:功能可用但可读性差
- Promise:链式调用,统一错误处理
- async/await:语法糖,最佳可读性
- AbortController:可取消的异步操作
- 顶层 await:模块级异步初始化
实际开发中,async/await + Promise.all 覆盖 95% 的场景。剩余场景用并发控制、重试、缓存等模式解决。