Appearance
17|JavaScript 异步编程
JavaScript 在浏览器中执行时,不能阻塞主线程——用户点击按钮、页面滚动、动画播放都依赖主线程响应。网络请求、文件读取、定时器等耗时操作必须以异步方式进行:发起操作后不等结果,继续执行后续代码,结果回来后再通过回调处理。
一、回调函数
最早的异步模式是把回调函数传给异步 API:
js
function fetchData(callback) {
setTimeout(() => {
callback("数据加载完成");
}, 1000);
}
fetchData((result) => {
console.log(result);
});多个异步操作串行时,回调嵌套越来越深:
js
getUser((user) => {
getOrders(user.id, (orders) => {
getDetails(orders[0].id, (details) => {
console.log(details);
});
});
});这种"回调地狱"(callback hell)代码难以阅读和维护。现代 JavaScript 用 Promise 和 async/await 替代嵌套回调。
二、Promise
Promise 是对异步操作的封装,表示一个尚未完成但预期将来会完成的操作:
js
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve("操作成功"); // 操作成功,传递结果
} else {
reject("操作失败"); // 操作失败,传递错误原因
}
}, 1000);
});Promise 有三种状态:
| 状态 | 含义 |
|---|---|
pending | 进行中,尚未完成 |
fulfilled | 已成功,调用 resolve 后 |
rejected | 已失败,调用 reject 后 |
状态一旦改变(从 pending 到 fulfilled 或 rejected),就不可再次改变。
then / catch
js
promise
.then((result) => {
console.log("成功:", result); // "操作成功"
})
.catch((err) => {
console.error("失败:", err);
});then 接收成功的回调,catch 接收失败的回调。两者可以链式调用。
链式调用
js
fetchUser()
.then((user) => fetchOrders(user.id))
.then((orders) => fetchDetails(orders[0].id))
.then((details) => {
console.log(details);
})
.catch((err) => {
console.error("任意一步出错:", err);
});链式调用中,then 的回调返回值会包装成新的 Promise 传给下一个 then。如果任意一步抛出异常或返回 rejected Promise,会跳过后续 then,直接进入 catch。
Promise.all
多个异步操作并行,等全部完成后统一处理:
js
const p1 = fetch("/api/servers");
const p2 = fetch("/api/tasks");
const p3 = fetch("/api/events");
Promise.all([p1, p2, p3])
.then(([res1, res2, res3]) => {
console.log("所有请求完成");
})
.catch((err) => {
console.error("任一请求失败:", err);
});Promise.all 在任意一个 Promise 失败时立即失败。如果希望"无论成功失败,等全部结束",用 Promise.allSettled:
js
Promise.allSettled([p1, p2, p3]).then((results) => {
results.forEach((r) => {
if (r.status === "fulfilled") {
console.log("成功:", r.value);
} else {
console.error("失败:", r.reason);
}
});
});Promise.race
多个 Promise 竞争,返回最先完成的那个(无论成功失败):
js
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error("超时")), 5000);
});
Promise.race([fetch("/api/data"), timeout])
.then((data) => console.log(data))
.catch((err) => console.error(err.message));常用于给异步操作加超时限制。
三、async / await
async/await 是 Promise 的语法糖,让异步代码看起来像同步代码:
js
async function loadDashboard() {
try {
const user = await fetchUser();
const orders = await fetchOrders(user.id);
const details = await fetchDetails(orders[0].id);
return details;
} catch (err) {
console.error("加载失败:", err.message);
return null;
}
}async函数总是返回一个 Promiseawait只能用在async函数内部await会暂停函数执行,等待 Promise 完成,然后把结果返回
并行 await
默认情况下 await 是串行的:
js
// 串行:total 耗时 = a + b + c
const a = await fetchA();
const b = await fetchB();
const c = await fetchC();没有依赖关系的请求应该并行:
js
// 并行:total 耗时 = max(a, b, c)
const [a, b, c] = await Promise.all([
fetchA(),
fetchB(),
fetchC(),
]);循环中的 await
js
// 串行执行(每次循环等上一个完成)
for (const url of urls) {
const data = await fetch(url);
console.log(data);
}
// 并行执行
const promises = urls.map((url) => fetch(url));
const results = await Promise.all(promises);需要按顺序处理结果时用 for...of + await;追求速度时用 Promise.all。
四、事件循环
JavaScript 是单线程的,异步操作通过事件循环(event loop)调度:
- 同步代码在主线程直接执行
- 异步操作(如
setTimeout、Promise)交给浏览器/Node.js 底层处理 - 底层完成操作后,把回调放入任务队列
- 主线程空闲时,从队列中取出回调执行
宏任务与微任务
任务队列分为两种:
| 类型 | 来源 | 优先级 |
|---|---|---|
| 微任务(microtask) | Promise.then/catch/finally、MutationObserver | 高 |
| 宏任务(macrotask) | setTimeout、setInterval、DOM 事件、I/O | 低 |
执行顺序:
- 执行当前宏任务(如同步脚本)
- 执行所有微任务队列中的任务
- 渲染页面(如果需要)
- 取下一个宏任务执行
js
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
console.log("4");
// 输出顺序:1, 4, 3, 2虽然 setTimeout 延迟是 0,但它的回调是宏任务;Promise 的 then 是微任务,微任务优先于下一个宏任务执行。
五、常见错误
在普通函数中用 await
js
// 错误:await 只能在 async 函数中使用
function load() {
const data = await fetchData();
}
// 正确
async function load() {
const data = await fetchData();
}忘记 await
js
// 错误:data 是 Promise,不是实际结果
async function load() {
const data = fetchData(); // 缺少 await
console.log(data.name); // undefined
}
// 正确
async function load() {
const data = await fetchData();
console.log(data.name);
}Promise 链中忘记 return
js
// 错误:then 中没 return,下一个 then 收到 undefined
fetchUser()
.then((user) => {
fetchOrders(user.id); // 缺少 return
})
.then((orders) => {
console.log(orders); // undefined
});
// 正确
fetchUser()
.then((user) => {
return fetchOrders(user.id);
})
.then((orders) => {
console.log(orders);
});用 forEach 加 await
js
// 错误:forEach 不会等待异步操作
urls.forEach(async (url) => {
await fetch(url);
});
console.log("全部完成"); // 这行会先执行,fetch 可能还没完成
// 正确:用 for...of
for (const url of urls) {
await fetch(url);
}
console.log("全部完成");