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(失败):操作出错,有失败原因。
状态一旦从 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();两个关键点:
async标记函数为异步函数,自动返回 Promise。await暂停函数,等 Promise resolve 后取值。只能在 async 函数里用(顶层 await 是 ES2022 才支持)。
实战中 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);
});三个常用的并发工具:
Promise.all:全部成功才算成功(任一失败就 reject)。Promise.race:第一个完成(成败都行)。常用作超时控制。Promise.allSettled:等所有完成(不管成败),返回每个结果的状态。适合"尽力而为"的场景。
常见错误:在循环里 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";
}
}用户体验要点:
- 给用户明确的错误提示("网络异常,请重试"比"Error: undefined"友好)。
- 重要操作提供重试按钮。
- 失败时降级(显示缓存数据、占位内容)。
- 用
finally隐藏加载动画,避免用户以为还在等。
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 的两个坑:
- 4xx/5xx 不会 reject,需要手动检查
res.ok。 - 默认不带 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 之前执行。
小结
- JS 是单线程,用事件循环处理异步。
- Promise 解决了回调地狱,有 pending/fulfilled/rejected 三状态。
async/await是异步的现代写法,用try/catch处理错误。Promise.all并发处理,显著提速。fetch是浏览器请求 API,记得检查res.ok。
下一篇我们系统过一遍 ES6+ 的新特性——把前面散见到的语法糖集中讲解。
← 上一篇 JavaScript BOM
下一篇 JavaScript ES6+ 新特性 →