JavaScript 异步与 Promise

异步编程是 JS 进阶的最大门槛,也是真实开发的核心——网络请求、文件读取、定时器、用户交互,几乎所有"耗时操作"都是异步的。这一章我们系统学习 JS 异步的演进:从回调函数,到 Promise,再到现代的 async/await。

1. 同步 vs 异步

JS 是单线程语言——一次只能做一件事。如果遇到耗时操作(如网络请求),整个页面会"卡住"等待,这是不可接受的。所以浏览器把耗时操作设计成异步——"先注册回调,继续干别的,操作完成后再通知我"。

// 同步代码:从上到下,一行执行完才执行下一行
console.log("1");
console.log("2");
console.log("3");
// 输出: 1 2 3(顺序确定)

// 异步代码:不会等待,先注册回调,继续往下执行
console.log("开始");
setTimeout(() => console.log("3 秒后"), 3000);
console.log("结束");
// 输出: 开始 → 结束 → (3 秒后)
// setTimeout 不会阻塞下面的代码!

// 异步的典型场景
// - 网络请求(ajax、fetch)
// - 文件读写(Node.js)
// - 定时器
// - 用户事件
// - 数据库操作
// 它们都"不会立刻返回结果",需要异步处理。

JS 用事件循环(event loop)处理异步:主线程同步代码先执行完,再从"任务队列"里取异步回调依次执行。理解事件循环需要单独一章,这里先记住"异步代码会在所有同步代码之后执行"

2. Promise 基础

Promise 是 ES6 引入的"将来某个时刻会有的值"的容器。它有三种状态:

状态一旦从 pending 变成 fulfilled 或 rejected,就不可逆。这种"一次性"特性,让异步结果的处理更可靠。

// Promise:表示"将来会有"的值
// 三种状态:pending(进行中)→ fulfilled(成功)/ rejected(失败)

// 1. 创建 Promise
const p = new Promise((resolve, reject) => {
  const success = true;
  if (success) {
    resolve("成功数据");     // 把状态变成 fulfilled
  } else {
    reject(new Error("失败原因")); // 变成 rejected
  }
});

// 2. 消费 Promise
p.then(data => {
  console.log("成功:", data);    // 成功: 成功数据
}).catch(err => {
  console.error("失败:", err);
}).finally(() => {
  console.log("无论成功失败都执行");
});

// 3. 实际场景:封装一个 ajax 请求
function fetchJSON(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.onload = () => {
      if (xhr.status === 200) {
        try {
          resolve(JSON.parse(xhr.responseText));
        } catch (e) {
          reject(e);
        }
      } else {
        reject(new Error("HTTP " + xhr.status));
      }
    };
    xhr.onerror = () => reject(new Error("网络错误"));
    xhr.send();
  });
}

then 接收成功结果,catch 接收失败,finally 无论成败都执行(常用于隐藏加载动画)。Promise 的真正威力在链式调用

3. 链式调用:解决回调地狱

Promise 之前的"回调嵌套"写法被称为回调地狱(callback hell)——代码层层缩进,极难阅读和维护:

// Promise 链式调用:解决"回调地狱"
// 回调地狱(老写法,地狱难读):
getUser(userId, function (user) {
  getOrders(user.id, function (orders) {
    getOrderDetail(orders[0].id, function (detail) {
      // 越缩越深...
    });
  });
});

// Promise 版(扁平化):
getUser(userId)
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetail(orders[0].id))
  .then(detail => console.log(detail))
  .catch(err => console.error("任一步失败都会到这里", err));

// then 返回值会传给下一个 then
// - 返回普通值:作为下一个 then 的参数
// - 返回 Promise:会等它完成再继续

Promise 通过 then 返回新 Promise,把"嵌套结构"变成"扁平链式"。而且任何一步出错都会跳到 catch,不用每层都写错误处理。

4. async/await:同步的写法,异步的本质

ES2017 引入的 async/await异步编程的最终方案——基于 Promise,但写法像同步代码一样直白:

// async/await:让异步代码"看起来像同步"
// ES2017 引入,基于 Promise,但写法更清晰

async function loadUser() {
  try {
    const user = await getUser(1);    // 等 Promise resolve
    const orders = await getOrders(user.id);
    console.log(orders);
  } catch (err) {
    console.error("出错:", err);
  }
}
loadUser();

// await 只能在 async 函数里用(顶层 await 是 ES2022)
// await 后面接 Promise,会暂停函数直到它 resolve

// 实战:用 fetch 加载接口
async function fetchData() {
  try {
    const res = await fetch("https://api.example.com/users");
    if (!res.ok) {
      throw new Error(`HTTP ${res.status}`);
    }
    const data = await res.json();
    console.log(data);
  } catch (err) {
    console.error("请求失败:", err);
  }
}
fetchData();

两个关键点:

实战中 90% 的异步代码用 async/await——可读性最好,错误处理用 try/catch 也直观。fetch 是浏览器内置的请求 API,返回 Promise,完美配合 async/await。

5. 并发处理:Promise.all

多个独立请求如果串行 await,会浪费等待时间。Promise.all 可以并行发起所有,一起等结果:

// Promise.all:并行执行多个 Promise,全部完成才进入 then
const p1 = fetch("/api/users").then(r => r.json());
const p2 = fetch("/api/products").then(r => r.json());
const p3 = fetch("/api/orders").then(r => r.json());

Promise.all([p1, p2, p3])
  .then(([users, products, orders]) => {
    console.log("三个接口都完成了");
  })
  .catch(err => console.log("任一失败就 reject"));

// async/await 版
async function loadAll() {
  const [users, products, orders] = await Promise.all([
    fetch("/api/users").then(r => r.json()),
    fetch("/api/products").then(r => r.json()),
    fetch("/api/orders").then(r => r.json())
  ]);
  console.log(users, products, orders);
}

// Promise.race:返回第一个完成的(无论成败)
const fastest = await Promise.race([
  fetch("/fast"),
  fetch("/slow"),
  new Promise((_, rej) => setTimeout(() => rej(new Error("超时")), 5000))
]);

// Promise.allSettled:等所有完成(不管成败)
const results = await Promise.allSettled([p1, p2]);
results.forEach(r => {
  if (r.status === "fulfilled") console.log(r.value);
  else console.error(r.reason);
});

三个常用的并发工具:

常见错误:在循环里 await 会让请求串行,失去并发优势:

// ❌ 串行:总耗时 = 每个请求时间之和
for (const url of urls) {
  const data = await fetch(url);
}

// ✅ 并行:总耗时 = 最慢那个请求的时间
const datas = await Promise.all(urls.map(url => fetch(url)));

6. 错误处理

异步代码必须处理错误——网络请求一定会失败,只是时间问题。Promise 用 catch,async/await 用 try/catch:

// 错误处理:异步代码必须有 catch

// Promise 版
fetchData()
  .then(data => processData(data))
  .catch(err => {
    console.error("失败:", err.message);
    // 显示错误提示给用户
    showErrorToast("加载失败,请重试");
  });

// async/await 版
async function load() {
  try {
    const data = await fetchData();
    return processData(data);
  } catch (err) {
    console.error("失败:", err);
    // 重试或降级
    return null;
  } finally {
    hideLoading();   // 无论成败都隐藏加载动画
  }
}

// 自定义错误:用 Error 对象(不要用字符串)
throw new Error("自定义错误信息");
// 或继承 Error 创建业务异常
class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

用户体验要点:

7. fetch:浏览器请求 API

// GET 请求
const res = await fetch("https://api.example.com/users");
// 注意:fetch 只在网络错误时 reject,4xx/5xx 不会!
// 必须手动检查 res.ok
if (!res.ok) {
  throw new Error(\`HTTP ${res.status}\`);
}
const data = await res.json();

// POST 请求
const res2 = await fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "小明", age: 20 })
});

// 带查询参数
const params = new URLSearchParams({ page: "1", size: "10" });
const res3 = await fetch("/api/users?" + params);

// 封装一个统一请求函数
async function request(url, options = {}) {
  const res = await fetch(url, {
    headers: { "Content-Type": "application/json", ...options.headers },
    ...options
  });
  if (!res.ok) {
    throw new Error(\`\`HTTP ${res.status}: ${await res.text()}\`\`);
  }
  return res.json();
}

fetch 的两个坑:

  1. 4xx/5xx 不会 reject,需要手动检查 res.ok
  2. 默认不带 cookie,需要加 credentials: "include"

很多项目会用 axios 替代 fetch——API 更友好,自动处理这些细节。

8. 实战:加载用户列表

async function loadUsers() {
  showLoading();
  try {
    // 5 秒超时
    const timeout = new Promise((_, rej) =>
      setTimeout(() => rej(new Error("超时")), 5000)
    );

    const res = await Promise.race([
      fetch("/api/users"),
      timeout
    ]);

    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const users = await res.json();

    renderUsers(users);
  } catch (err) {
    showError(err.message);
    // 降级:显示缓存
    const cached = JSON.parse(localStorage.getItem("users") || "[]");
    if (cached.length) renderUsers(cached);
  } finally {
    hideLoading();
  }
}

// 用 IIFE 立即执行(顶层 await 在浏览器和 build 工具支持不一)
(async () => {
  await loadUsers();
})();

9. 微任务与宏任务(进阶)

console.log("1 同步");

setTimeout(() => console.log("2 宏任务"), 0);

Promise.resolve().then(() => console.log("3 微任务"));

console.log("4 同步");

// 输出顺序: 1 → 4 → 3 → 2
// 同步代码先跑完 → 微任务队列(Promise)→ 宏任务队列(setTimeout)

// 简化记忆:
// 微任务:Promise.then/catch/finally、MutationObserver
// 宏任务:setTimeout、setInterval、I/O、UI 事件
// 每轮事件循环:跑一个宏任务 → 清空所有微任务 → 渲染

理解微任务/宏任务,能解释一些"看似诡异"的执行顺序——比如 Promise 的 then 总在 setTimeout 之前执行。

小结

下一篇我们系统过一遍 ES6+ 的新特性——把前面散见到的语法糖集中讲解。

← 上一篇 JavaScript BOM

下一篇 JavaScript ES6+ 新特性

✈️💬