引言:从"回调地狱"到优雅同步
在现代软件开发中,异步操作无处不在。无论是网络请求、文件读写、数据库查询,还是定时器任务,都需要处理异步逻辑。JavaScript 的异步编程经历了从回调函数到 Promise,再到 async/await 的演进历程。自 ES2017(ES8)正式引入以来,async/await 已成为处理异步操作的首选方案,它让异步代码拥有了同步代码般的可读性和可维护性。
一、为什么需要 async/await?
1.1 异步编程的演进痛点
回调函数时代(Callback Hell)
javascript
// 典型的回调地狱
fs.readFile('file1.txt', (err, data1) => {
if (err) throw err;
fs.readFile('file2.txt', (err, data2) => {
if (err) throw err;
fs.readFile('file3.txt', (err, data3) => {
if (err) throw err;
console.log(data1, data2, data3);
});
});
});
问题:代码嵌套层级深、错误处理分散、逻辑难以追踪。
Promise 时代(链式调用)
ini
// Promise 链式调用
fetch('/api/user')
.then(response => response.json())
.then(user => fetch(`/api/posts/${user.id}`))
.then(response => response.json())
.then(posts => console.log(posts))
.catch(err => console.error(err));
改进:解决了嵌套问题,但 .then() 链依然不够直观,错误处理需要额外的 .catch()。
async/await 时代(同步风格)
javascript
// async/await 写法
async function getUserPosts() {
try {
const userResponse = await fetch('/api/user');
const user = await userResponse.json();
const postsResponse = await fetch(`/api/posts/${user.id}`);
const posts = await postsResponse.json();
console.log(posts);
} catch (err) {
console.error(err);
}
}
优势:代码像同步一样线性执行,错误处理统一,调试更友好。
二、核心概念与语法
2.1 async 关键字
async 用于声明一个异步函数,它有以下几个特性:
- 自动返回 Promise :即使函数返回普通值,也会自动包装成
Promise.resolve(value) - 允许使用 await:只有在 async 函数内部才能使用 await 关键字
- 非阻塞执行:async 函数不会阻塞主线程
javascript
// 示例 1:返回值自动包装为 Promise
async function sayHello() {
return 'Hello';
// 等价于 return Promise.resolve('Hello');
}
sayHello().then(msg => console.log(msg)); // 输出: Hello
// 示例 2:抛出错误会返回 rejected Promise
async function throwError() {
throw new Error('Something wrong');
}
throwError().catch(err => console.error(err.message)); // 输出: Something wrong
2.2 await 关键字
await 用于等待 Promise 完成,它只能在 async 函数内部使用:
- 暂停执行:遇到 await 时,函数会暂停执行,直到 Promise resolved
- 获取结果:await 表达式的值是 Promise 的 resolved 值
- 错误传播:如果 Promise rejected,await 会抛出异常
csharp
async function fetchData() {
// 等待 Promise 完成
const response = await fetch('/api/data');
// 获取 resolved 值
const data = await response.json();
return data;
}
三、底层原理:状态机与事件循环
3.1 async 函数的转换机制
当 JavaScript 引擎遇到 async 函数时,会将其转换为一个状态机。每个 await 表达式都是状态机的一个检查点:
ini
// 原始代码
async function example() {
const a = await promise1();
const b = await promise2(a);
return a + b;
}
// 近似转换(简化版)
function example() {
return new Promise((resolve, reject) => {
let a, b;
promise1().then(
value => {
a = value;
return promise2(a);
},
reject
).then(
value => {
b = value;
resolve(a + b);
},
reject
);
});
}
3.2 await 的工作流程
- 求值:计算 await 右侧的表达式(必须是 Promise 或可转换为 Promise 的值)
- 暂停:如果 Promise 未完成,暂停 async 函数执行,将控制权交还给事件循环
- 订阅:注册 Promise 的完成回调
- 恢复:当 Promise settled 后,恢复函数执行,继续后续代码
javascript
async function workflow() {
console.log('1. 开始');
const result = await new Promise(resolve => {
setTimeout(() => {
console.log('2. Promise 完成');
resolve('数据');
}, 1000);
});
console.log('3. 继续执行,result =', result);
}
workflow();
// 输出顺序:
// 1. 开始
// (1 秒后) 2. Promise 完成
// 3. 继续执行,result = 数据
四、错误处理最佳实践
4.1 try-catch 模式(推荐)
这是最常用且最清晰的错误处理方式:
javascript
async function safeFetch(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('请求失败:', error.message);
// 可以选择重新抛出或返回默认值
throw error; // 或 return null;
}
}
4.2 .catch() 链式处理
适用于简单的场景:
javascript
async function fetchData() {
return await fetch('/api/data')
.then(res => res.json())
.catch(err => {
console.error(err);
return null; // 返回默认值
});
}
4.3 全局错误处理
在 Node.js 或框架中设置全局处理器:
javascript
// Node.js 未捕获的 Promise rejection
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 Promise rejection:', reason);
});
// 浏览器窗口级别
window.addEventListener('unhandledrejection', event => {
console.error('未处理的 Promise rejection:', event.reason);
});
五、高级技巧与实战场景
5.1 并行执行 vs 串行执行
❌ 错误的串行写法(效率低)
javascript
async function fetchUsersSlow() {
const user1 = await fetch('/api/user/1').then(r => r.json());
const user2 = await fetch('/api/user/2').then(r => r.json());
const user3 = await fetch('/api/user/3').then(r => r.json());
// 总耗时 = 3 个请求时间之和
return [user1, user2, user3];
}
✅ 正确的并行写法(效率高)
less
async function fetchUsersFast() {
const [user1, user2, user3] = await Promise.all([
fetch('/api/user/1').then(r => r.json()),
fetch('/api/user/2').then(r => r.json()),
fetch('/api/user/3').then(r => r.json())
]);
// 总耗时 ≈ 最慢的那个请求时间
return [user1, user2, user3];
}
5.2 循环中的异步操作
❌ 避免在循环中串行 await
javascript
// 低效:逐个等待
async function processItems(items) {
const results = [];
for (const item of items) {
const result = await processItem(item); // 串行执行
results.push(result);
}
return results;
}
✅ 使用 Promise.all 并行处理
javascript
// 高效:并行执行
async function processItems(items) {
const promises = items.map(item => processItem(item));
return await Promise.all(promises);
}
⚠️ 需要控制并发数时使用限制器
ini
async function processWithConcurrency(items, limit = 5) {
const results = [];
const executing = new Set();
for (const item of items) {
const promise = processItem(item).then(result => {
executing.delete(promise);
return result;
});
results.push(promise);
executing.add(promise);
if (executing.size >= limit) {
await Promise.race(executing); // 等待其中一个完成
}
}
return Promise.all(results);
}
5.3 超时控制
javascript
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('请求超时');
}
throw error;
}
}
5.4 重试机制
javascript
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`Status: ${response.status}`);
return await response.json();
} catch (error) {
if (i === retries - 1) throw error; // 最后一次失败则抛出
console.warn(`重试 ${i + 1}/${retries}`, error.message);
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); // 指数退避
}
}
}
六、常见陷阱与注意事项
6.1 忘记加 async
csharp
// ❌ 错误:在非 async 函数中使用 await
function getData() {
const data = await fetch('/api/data'); // SyntaxError
return data;
}
// ✅ 正确
async function getData() {
const data = await fetch('/api/data');
return data;
}
6.2 不必要的 await
csharp
// ❌ 冗余:直接返回 Promise 即可
async function fetchData() {
return await fetch('/api/data'); // 多此一举
}
// ✅ 简洁
async function fetchData() {
return fetch('/api/data'); // 自动包装为 Promise
}
6.3 并行误写为串行
javascript
// ❌ 串行执行(慢)
async function parallelWrong() {
const data1 = await fetch('/api/1');
const data2 = await fetch('/api/2');
const data3 = await fetch('/api/3');
}
// ✅ 并行执行(快)
async function parallelRight() {
const [data1, data2, data3] = await Promise.all([
fetch('/api/1'),
fetch('/api/2'),
fetch('/api/3')
]);
}
6.4 顶层 await 的限制
顶层 await(Top-level await)允许在模块顶层直接使用 await,但有以下限制:
javascript
// ✅ ES 模块中可以使用(Node.js 14.8+,现代浏览器)
const data = await fetch('/api/data').then(r => r.json());
// ❌ CommonJS 或脚本标签中不支持
// 会报 SyntaxError
七、跨语言视角:其他语言的 async/await
async/await 并非 JavaScript 独有,许多现代编程语言都采用了类似机制:
| 语言 | 引入版本 | 特点 |
|---|---|---|
| C# | 5.0 (2012) | 最早实现之一,基于 Task 类型 |
| Python | 3.5 (2015) | asyncio 库,需配合事件循环 |
| Java | 无原生支持 | 通过 CompletableFuture 模拟 |
| Rust | 1.39 (2019) | 基于 Future trait,需运行时(如 tokio) |
| Go | 无原生支持 | 使用 goroutine + channel 模式 |
| Kotlin | 1.3 (2018) | 协程(Coroutines),轻量级线程 |
.NET 中的特殊优化:
ConfigureAwait(false):避免死锁,提升性能ValueTask<T>:减少堆分配,适合高频异步操作IAsyncEnumerable<T>:异步流式处理
八、性能优化建议
8.1 避免过度使用 async
不是所有函数都需要标记为 async:
csharp
// ❌ 不必要
async function getValue() {
return 42; // 同步值却被包装成 Promise
}
// ✅ 仅在需要 await 时使用 async
async function getValue() {
return await someAsyncOperation();
}
8.2 合理使用 Promise.allSettled
当需要等待所有 Promise 完成(无论成功失败)时:
javascript
const results = await Promise.allSettled([
fetch('/api/1'),
fetch('/api/2'),
fetch('/api/3')
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`请求 ${index} 成功`, result.value);
} else {
console.error(`请求 ${index} 失败`, result.reason);
}
});
8.3 内存泄漏预防
确保清理定时器和事件监听器:
javascript
async function monitor() {
const intervalId = setInterval(() => {
// 监控逻辑
}, 1000);
try {
while (true) {
await checkStatus();
await sleep(5000);
}
} finally {
clearInterval(intervalId); // 确保清理
}
}
九、实战案例:封装通用请求工具
javascript
// request.js
class HttpClient {
constructor(baseURL = '') {
this.baseURL = baseURL;
}
async request(url, options = {}) {
const fullUrl = `${this.baseURL}${url}`;
const config = {
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
};
if (options.body && config.method !== 'GET') {
config.body = JSON.stringify(options.body);
}
try {
const response = await fetch(fullUrl, config);
if (!response.ok) {
throw new HttpError(
`HTTP ${response.status}`,
response.status,
await response.text()
);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
return await response.text();
} catch (error) {
if (error instanceof HttpError) throw error;
throw new NetworkError('网络请求失败', error);
}
}
async get(url, options) {
return this.request(url, { ...options, method: 'GET' });
}
async post(url, body, options) {
return this.request(url, { ...options, method: 'POST', body });
}
async put(url, body, options) {
return this.request(url, { ...options, method: 'PUT', body });
}
async delete(url, options) {
return this.request(url, { ...options, method: 'DELETE' });
}
}
class HttpError extends Error {
constructor(message, status, responseBody) {
super(message);
this.name = 'HttpError';
this.status = status;
this.responseBody = responseBody;
}
}
class NetworkError extends Error {
constructor(message, originalError) {
super(message);
this.name = 'NetworkError';
this.originalError = originalError;
}
}
// 使用示例
const api = new HttpClient('https://api.example.com');
async function getUserProfile(userId) {
try {
const user = await api.get(`/users/${userId}`);
const posts = await api.get(`/users/${userId}/posts`);
return { user, posts };
} catch (error) {
if (error instanceof HttpError) {
console.error(`API 错误 ${error.status}:`, error.message);
} else if (error instanceof NetworkError) {
console.error('网络错误:', error.message);
}
throw error;
}
}
十、总结与展望
核心要点回顾
- async/await 是基于 Promise 的语法糖,让异步代码拥有同步的可读性
- async 函数自动返回 Promise,await 用于等待 Promise 完成
- 错误处理优先使用 try-catch,保持代码清晰
- 并行操作用 Promise.all,避免不必要的串行等待
- 注意性能陷阱:避免过度使用 async、防止内存泄漏
未来趋势
- 顶层 await 普及:随着 ES 模块成为标准,顶层 await 将更广泛使用
- 异步迭代器增强 :
for await...of在处理流式数据时更加重要 - 与其他特性结合:如 Pattern Matching、Records & Tuples 等新提案
- 跨语言统一:不同语言的 async/await 实现趋于一致,降低学习成本
最后建议
"用同步的思维写异步代码,但要时刻记住它本质是异步的。"
掌握 async/await 不仅是学会两个关键字,更是理解事件循环、Promise 状态机、非阻塞 I/O 等核心概念。在实际项目中,结合具体场景选择合适的模式(并行/串行/重试/超时),才能写出既优雅又高效的异步代码。