JavaScript异步编程:从回调到Async/Await的演进

前言

JavaScript 是单线程语言,但现代 Web 应用却需要处理大量的异步操作:网络请求、文件读写、定时任务……异步编程是每个 JavaScript 开发者必须掌握的核心能力。

这篇文章将带你回顾 JavaScript 异步编程的演进历程,理解每种方案的设计动机和使用场景。

为什么需要异步?

JavaScript 运行在浏览器中,如果所有操作都是同步的,会发生什么?

1
2
3
// ❌ 同步请求会阻塞整个页面
const data = syncFetch("/api/user"); // 页面冻结,直到请求完成
console.log(data); // 用户等了3秒才能看到结果

异步编程的核心思想是:不要等待,先去做别的事情,结果准备好了再回来处理。

第一代:回调函数(Callback)

基本用法

1
2
3
4
5
6
7
8
// 最简单的异步:setTimeout
console.log("开始");
setTimeout(() => {
console.log("1秒后执行");
}, 1000);
console.log("结束");

// 输出顺序:开始 → 结束 → 1秒后执行

回调地狱(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
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 库)
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
// 1. 回调 —— 嵌套地狱
getData(a, (b) => {
getData(b, (c) => {
getData(c, (d) => {
console.log(d);
});
});
});

// 2. Promise —— 链式调用
getData(a)
.then((b) => getData(b))
.then((c) => getData(c))
.then((d) => console.log(d));

// 3. Generator —— 需要执行器
function* run() {
const b = yield getData(a);
const c = yield getData(b);
const d = yield getData(c);
console.log(d);
}

// 4. Async/Await —— 最优雅
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
// ❌ forEach 不会等待 async
items.forEach(async (item) => {
await processItem(item); // 不会按顺序等待
});

// ✅ for...of 可以按顺序等待
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]);
}

// 3秒超时
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"); // 同步

// 输出顺序:1 → 4 → 3 → 2

最佳实践

  1. 优先使用 async/await:代码更清晰,错误处理更自然
  2. 注意并行与串行:无依赖的操作用 Promise.all 并行执行
  3. 始终处理错误:使用 try/catch 或 .catch() 捕获异常
  4. 避免在循环中串行 await:除非确实需要顺序执行
  5. 合理使用超时:网络请求等操作应设置超时
  6. 理解事件循环:这是写出正确异步代码的基础

总结

JavaScript 异步编程的演进,本质上是让异步代码越来越像同步代码的过程:

1
2
回调 → Promise → Generator → Async/Await
混乱 链式 暂停执行 同步写法

每种方案都有其适用场景,理解演进历程有助于我们在实际开发中做出更好的选择。Async/Await 不是银弹,但它确实是目前最优雅的异步编程方案。