"JS 是单线程的,却能处理大量并发"--理解异步是进阶前端的第一道坎。本文从事件循环讲起,带你彻底搞懂 Promise 和 async/await。

1. 为什么需要异步?

JavaScript 运行在单线程上。如果网络请求、文件读取都同步执行,页面会在等待时完全卡死。异步的核心理念是:先干别的,等结果回来再处理

console.log("开始");
setTimeout(() => console.log("3 秒后"), 3000);
console.log("结束");
// 输出顺序: 开始 → 结束 → 3 秒后

2. 事件循环(Event Loop)

JS 运行时维护着调用栈(Call Stack)和任务队列(Task Queue):

  1. 同步代码依次进入调用栈执行;
  2. 异步任务(定时器、网络请求等)完成回调进入任务队列;
  3. 调用栈清空后,事件循环把队列中的任务取出来执行。

还有一个特殊的 微任务队列(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);
  }
}

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% 的"输出顺序题"都能秒答。