"JS 是单线程的,却能处理大量并发"--理解异步是进阶前端的第一道坎。本文从事件循环讲起,带你彻底搞懂 Promise 和 async/await。
1. 为什么需要异步?
JavaScript 运行在单线程上。如果网络请求、文件读取都同步执行,页面会在等待时完全卡死。异步的核心理念是:先干别的,等结果回来再处理。
console.log("开始");
setTimeout(() => console.log("3 秒后"), 3000);
console.log("结束");
// 输出顺序: 开始 → 结束 → 3 秒后
2. 事件循环(Event Loop)
JS 运行时维护着调用栈(Call Stack)和任务队列(Task Queue):
- 同步代码依次进入调用栈执行;
- 异步任务(定时器、网络请求等)完成回调进入任务队列;
- 调用栈清空后,事件循环把队列中的任务取出来执行。
还有一个特殊的 微任务队列(Microtask Queue):Promise 的回调进微任务,优先于宏任务(如 setTimeout)执行。
Promise.resolve().then(() => console.log("微任务"));
setTimeout(() => console.log("宏任务"));
// 输出顺序: 微任务 → 宏任务
3. 回调地狱 → Promise
早期用回调嵌套,代码像圣诞树一样膨胀:
// ❌ 回调地狱
getUser(id, (user) => {
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
console.log(comments);
});
});
});
// ✅ Promise 链式调用
getUser(id)
.then((user) => getPosts(user.id))
.then((posts) => getComments(posts[0].id))
.then((comments) => console.log(comments))
.catch((err) => console.error(err));
4. async/await:更优雅的写法
async/await 是 Promise 的语法糖,让异步代码看起来像同步代码:
async function loadComments(id) {
try {
const user = await getUser(id);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
return comments;
} catch (err) {
console.error("加载失败:", err);
}
}
await只能在async函数中使用;- 错误处理用
try/catch,等价于 Promise 的.catch(); - 互不依赖的请求要并行:
await Promise.all([...])。
5. 实战:并发请求
// 串行(慢): 逐个等待
const a = await fetch("/api/a");
const b = await fetch("/api/b");
// 并行(快): 同时发起
const [a, b] = await Promise.all([
fetch("/api/a"),
fetch("/api/b"),
]);
6. 并发控制进阶
除了 Promise.all,还有两个常用的组合器:
// allSettled: 等所有请求"落地"再处理,不因单个失败而中断
const results = await Promise.allSettled([
fetch("/api/a"), fetch("/api/b"), fetch("/api/c"),
]);
results.forEach((r) => {
if (r.status === "fulfilled") console.log("成功:", r.value);
else console.log("失败:", r.reason);
});
// race: 竞速,常用于超时控制
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("请求超时")), 3000)
);
const data = await Promise.race([fetch("/api/data"), timeout]);
记忆口诀:并行用 all,容忍部分失败用 allSettled,要超时/竞速用 race。再配合 AbortController 就能真正取消请求——这是面试高频考点。
💡 记忆口诀:同步优先,微任务其次,宏任务最后。理解了这个顺序,面试里 90% 的"输出顺序题"都能秒答。