JavaScript 异步编程:async/await 与 Promise 链
JavaScript 中的异步操作用于处理耗时任务(如网络请求、文件读取),避免阻塞主线程。Promise 是处理异步的基础机制,而 async/await 是基于 Promise 的语法糖,让异步代码写起来像同步代码一样直观。
理解 Promise 链
创建一个 Promise 对象,它代表一个未来可能完成或失败的操作:
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve("数据加载成功");
} else {
reject("数据加载失败");
}
}, 1000);
});
};
调用这个函数并使用 .then() 和 .catch() 处理结果:
- 调用
fetchData()。 - 使用
.then(result => console.log(result))处理成功结果。 - 使用
.catch(error => console.error(error))捕获错误。
当多个异步操作需要按顺序执行时,可以链式调用 .then():
fetchData()
.then(message => {
console.log(message); // "数据加载成功"
return "下一步处理";
})
.then(next => {
console.log(next); // "下一步处理"
return 42;
})
.catch(err => {
console.error(err);
});
每个 .then() 返回的值会自动包装成一个新的 Promise,供下一个 .then() 使用。
使用 async/await 简化异步代码
async/await 让你用同步风格写异步逻辑,代码更易读。
定义一个 async 函数:
async function processData() {
try {
const result = await fetchData();
console.log(result); // "数据加载成功"
const next = "下一步处理";
console.log(next);
return 42;
} catch (error) {
console.error(error);
}
}
调用该函数:
processData().then(value => console.log(value)); // 42
关键规则:
- 在
async函数内部使用await关键字等待Promise完成。 - 必须将
await放在try...catch块中,否则错误无法被捕获。 await后面可以是任何Promise,也可以是普通值(会被自动包装)。
对比两种写法的实际场景
假设你需要依次完成三个步骤:登录 → 获取用户信息 → 获取订单列表。
使用 Promise 链
login()
.then(token => fetchUser(token))
.then(user => fetchOrders(user.id))
.then(orders => console.log("订单:", orders))
.catch(err => console.error("流程出错:", err));
使用 async/await
async function getUserOrders() {
try {
const token = await login();
const user = await fetchUser(token);
const orders = await fetchOrders(user.id);
console.log("订单:", orders);
} catch (err) {
console.error("流程出错:", err);
}
}
两者功能完全等价,但 async/await 更接近人类阅读顺序,逻辑更清晰。
错误处理的关键差异
在 Promise 链中,任何一个 .then() 抛出错误或返回被拒绝的 Promise,都会跳转到最近的 .catch()。
在 async/await 中,只要 await 的 Promise 被拒绝,就会抛出异常,必须用 try...catch 捕获。
若忘记 try...catch,错误会冒泡到调用者:
async function risky() {
await Promise.reject("出错了"); // 未捕获
}
risky().catch(err => console.log(err)); // 正确方式:在调用处捕获
并行执行多个异步操作
如果多个异步任务彼此独立,应并行执行以提升性能。
不要这样顺序等待:
// ❌ 低效:串行
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();
应该使用 Promise.all():
// ✅ 高效:并行
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
Promise.all() 接收一个 Promise 数组,当所有都成功时返回结果数组;任一失败则整体失败。
若希望部分失败也不影响其他任务,可使用 Promise.allSettled():
const results = await Promise.allSettled([
fetchUser(),
fetchPosts(),
fetchComments()
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`任务 ${index} 成功:`, result.value);
} else {
console.log(`任务 ${index} 失败:`, result.reason);
}
});
常见陷阱与最佳实践
-
避免在循环中直接使用
await(会导致串行执行):// ❌ 串行,慢 for (const url of urls) { const data = await fetch(url); process(data); } // ✅ 并行 const promises = urls.map(url => fetch(url)); const results = await Promise.all(promises); results.forEach(process); -
不要混淆
return await和return:- 在
async函数中,return value等价于return Promise.resolve(value)。 return await promise通常多余,除非在try...catch中需要确保错误被捕获。
- 在
-
顶层 await(Top-level await)仅在 ES 模块中可用:
// 只能在 .mjs 文件或 <script type="module"> 中使用 const config = await loadConfig();
何时选择哪种写法
| 场景 | 推荐写法 |
|---|---|
| 简单的单个异步调用 | async/await |
| 多个依赖前一步结果的异步操作 | async/await |
| 需要并行执行多个独立任务 | Promise.all() + await |
| 需要在链中动态决定下一步(如条件分支) | Promise 链更灵活 |
| 兼容旧环境(不支持 async/await) | Promise 链 |
// 动态分支示例:Promise 链更自然
fetchConfig()
.then(config => {
if (config.featureEnabled) {
return enableFeature();
} else {
return disableFeature();
}
})
.then(result => console.log(result));
而在 async/await 中也能实现,但需额外变量:
async function handleConfig() {
const config = await fetchConfig();
let result;
if (config.featureEnabled) {
result = await enableFeature();
} else {
result = await disableFeature();
}
console.log(result);
}
两者均可,按团队习惯选择。
调试技巧
启用浏览器或 Node.js 的异步栈追踪(Async Stack Traces),能清晰看到 await 调用链。
在 Node.js 中,确保使用较新版本(v12+ 默认开启)。
在 Chrome DevTools 中,勾选 “Async” 选项以查看完整的异步调用栈。
遇到未处理的 Promise 拒绝时,监听 unhandledrejection 事件:
window.addEventListener('unhandledrejection', event => {
console.error('未捕获的异步错误:', event.reason);
event.preventDefault(); // 阻止默认报错
});
Node.js 中使用:
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的 rejection:', reason);
});
性能与内存注意事项
async/await 和 Promise 链在性能上几乎没有差异,因为 async/await 编译后就是 Promise。
但要注意:
- 避免创建不必要的
Promise包装。 - 不要在高频循环中滥用
await,可能导致大量微任务堆积。 - 及时处理错误,防止内存泄漏(未处理的 rejected Promise 会保留引用)。
例如,以下写法会泄漏:
function leaky() {
someAsyncOperation().catch(() => {}); // 错误被静默忽略,但 Promise 对象可能长期驻留
}
正确做法是显式处理或记录错误。
迁移旧代码的策略
将回调风格(Callback Hell)迁移到 Promise,再升级到 async/await:
-
封装回调函数为返回
Promise的函数:const readFilePromise = (path) => { return new Promise((resolve, reject) => { fs.readFile(path, 'utf8', (err, data) => { if (err) reject(err); else resolve(data); }); }); }; -
替换所有回调调用为
await:async function processFile() { const content = await readFilePromise('data.txt'); return content.toUpperCase(); }
现代 Node.js 已内置 util.promisify 辅助转换:
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
测试异步代码
使用 Jest 或 Mocha 等测试框架时,标记测试函数为 async:
test('should fetch user', async () => {
const user = await fetchUser('123');
expect(user.name).toBe('Alice');
});
或返回 Promise:
test('should fetch user', () => {
return fetchUser('123').then(user => {
expect(user.name).toBe('Alice');
});
});
切勿在测试中遗漏 await 或 return,否则测试会提前结束,无法捕获异步错误。
总结核心原则
- 优先使用
async/await编写新代码,结构清晰。 - 善用
Promise.all实现并行,避免不必要的串行等待。 - 始终处理异步错误,用
try...catch或.catch()。 - 理解
await只能在async函数内使用。 - 调试时开启异步栈追踪,快速定位问题源头。

暂无评论,快来抢沙发吧!