前言
JavaScript 是单线程语言,但现代 Web 应用却需要处理大量的异步操作:网络请求、文件读写、定时任务……异步编程是每个 JavaScript 开发者必须掌握的核心能力。
这篇文章将带你回顾 JavaScript 异步编程的演进历程,理解每种方案的设计动机和使用场景。
为什么需要异步?
JavaScript 运行在浏览器中,如果所有操作都是同步的,会发生什么?
1 2 3
| const data = syncFetch("/api/user"); console.log(data);
|
异步编程的核心思想是:不要等待,先去做别的事情,结果准备好了再回来处理。
第一代:回调函数(Callback)
基本用法
1 2 3 4 5 6 7 8
| console.log("开始"); setTimeout(() => { console.log("1秒后执行"); }, 1000); console.log("结束");
|
回调地狱(Callback Hell)
当多个异步操作有依赖关系时,回调会层层嵌套:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| getUser( userId, (user) => { getOrders( user.id, (orders) => { getOrderDetail( orders[0].id, (detail) => { getComments( detail.id, (comments) => { renderPage(user, orders, detail, comments); }, (err) => { console.error("获取评论失败", err); }, ); }, (err) => { console.error("获取订单详情失败", err); }, ); }, (err) => { console.error("获取订单失败", err); }, ); }, (err) => { console.error("获取用户失败", err); }, );
|
回调的问题
| 问题 |
说明 |
| 嵌套过深 |
代码形成”金字塔”,难以阅读 |
| 错误处理繁琐 |
每层都需要单独处理错误 |
| 无法返回 |
回调中无法使用 return 抛出结果 |
| 无法组合 |
多个异步操作并行、竞速等难以实现 |
第二代:Promise
什么是 Promise?
Promise 是一个代表异步操作最终完成(或失败)的对象。它有三种状态:
- Pending:初始状态,既不是成功也不是失败
- Fulfilled:操作成功完成
- Resolved:操作失败
1 2 3 4 5 6 7 8 9 10 11
| const promise = new Promise((resolve, reject) => { setTimeout(() => { const success = true; if (success) { resolve("操作成功"); } else { reject(new Error("操作失败")); } }, 1000); });
|
链式调用
Promise 最大的改进是支持链式调用,解决了回调嵌套问题:
1 2 3 4 5 6
| getUser(userId) .then((user) => getOrders(user.id)) .then((orders) => getOrderDetail(orders[0].id)) .then((detail) => getComments(detail.id)) .then((comments) => renderPage(comments)) .catch((err) => console.error("出错了:", err));
|
常用方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Promise.all([p1, p2, p3]).then((results) => console.log("全部完成:", results));
Promise.race([p1, p2, p3]).then((result) => console.log("最快的:", result));
Promise.allSettled([p1, p2, p3]).then((results) => { results.forEach((result) => { if (result.status === "fulfilled") { console.log("成功:", result.value); } else { console.log("失败:", result.reason); } }); });
Promise.any([p1, p2, p3]).then((result) => console.log("第一个成功:", result));
|
Promise 的局限
- 语义不够直观:链式调用虽然比回调好,但仍然是以”数据处理”为核心,而非”流程控制”
- 无法取消:Promise 一旦创建就会执行,无法中途取消
- 调试困难:在 then 链中设置断点不如同步代码直观
过渡代:Generator
Generator 函数可以暂停执行,配合执行器可以实现类似同步的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function* fetchUserData() { try { const user = yield getUser(userId); const orders = yield getOrders(user.id); const detail = yield getOrderDetail(orders[0].id); const comments = yield getComments(detail.id); return { user, orders, detail, comments }; } catch (err) { console.error("出错了:", err); } }
co(fetchUserData).then((data) => renderPage(data));
|
Generator 方案虽然优雅,但需要额外的执行器,没有成为主流方案。
第三代:Async/Await
基本用法
Async/Await 是 Generator 的语法糖,内置执行器,写法更像同步代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| async function fetchUserData() { try { const user = await getUser(userId); const orders = await getOrders(user.id); const detail = await getOrderDetail(orders[0].id); const comments = await getComments(detail.id); return { user, orders, detail, comments }; } catch (err) { console.error("出错了:", err); } }
const data = await fetchUserData(); renderPage(data);
|
对比四种方案
同样的需求,四种写法的对比:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| getData(a, (b) => { getData(b, (c) => { getData(c, (d) => { console.log(d); }); }); });
getData(a) .then((b) => getData(b)) .then((c) => getData(c)) .then((d) => console.log(d));
function* run() { const b = yield getData(a); const c = yield getData(b); const d = yield getData(c); console.log(d); }
async function run() { const b = await getData(a); const c = await getData(b); const d = await getData(c); console.log(d); }
|
高级技巧
并行执行
1 2 3 4 5 6
| const user = await getUser(); const posts = await getPosts();
const [user, posts] = await Promise.all([getUser(), getPosts()]);
|
循环中的异步
1 2 3 4 5 6 7 8 9 10 11 12
| items.forEach(async (item) => { await processItem(item); });
for (const item of items) { await processItem(item); }
await Promise.all(items.map((item) => processItem(item)));
|
超时控制
1 2 3 4 5 6 7 8 9
| function withTimeout(promise, ms) { const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("超时")), ms), ); return Promise.race([promise, timeout]); }
const data = await withTimeout(fetch("/api/data"), 3000);
|
重试机制
1 2 3 4 5 6 7 8 9 10 11 12
| async function retry(fn, maxRetries = 3, delay = 1000) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (err) { if (i === maxRetries - 1) throw err; await new Promise((r) => setTimeout(r, delay * (i + 1))); } } }
const data = await retry(() => fetch("/api/data").then((r) => r.json()));
|
事件循环:理解异步的底层机制
所有异步方案都建立在事件循环(Event Loop)之上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ┌───────────────────────┐ │ 调用栈 (Call Stack) │ │ ┌─────────────────┐ │ │ │ 执行同步代码 │ │ │ └─────────────────┘ │ └───────────┬───────────┘ │ ▼ ┌───────────────────────┐ │ 微任务队列 (Microtask) │ │ Promise.then / await │ │ MutationObserver │ │ process.nextTick │ └───────────┬───────────┘ │ ▼ ┌───────────────────────┐ │ 宏任务队列 (Macrotask) │ │ setTimeout / setInterval │ │ I/O 操作 │ │ UI 渲染 │ └───────────────────────┘
|
执行顺序:同步代码 → 微任务(清空)→ 宏任务(一个)→ 微任务(清空)→ …
1 2 3 4 5 6 7 8 9 10 11 12
| console.log("1");
setTimeout(() => console.log("2"));
Promise.resolve().then(() => { console.log("3"); });
console.log("4");
|
最佳实践
- 优先使用 async/await:代码更清晰,错误处理更自然
- 注意并行与串行:无依赖的操作用
Promise.all 并行执行
- 始终处理错误:使用 try/catch 或 .catch() 捕获异常
- 避免在循环中串行 await:除非确实需要顺序执行
- 合理使用超时:网络请求等操作应设置超时
- 理解事件循环:这是写出正确异步代码的基础
总结
JavaScript 异步编程的演进,本质上是让异步代码越来越像同步代码的过程:
1 2
| 回调 → Promise → Generator → Async/Await 混乱 链式 暂停执行 同步写法
|
每种方案都有其适用场景,理解演进历程有助于我们在实际开发中做出更好的选择。Async/Await 不是银弹,但它确实是目前最优雅的异步编程方案。