回调地狱
在 Promise 出现之前,异步编程的主流方式是回调函数。这导致了一种臭名昭著的反模式——回调地狱:
getData(function(a) {
getMoreData(a, function(b) {
getMoreData(b, function(c) {
getMoreData(c, function(d) {
console.log('done:', d);
});
});
});
});
回调地狱的问题:
- 代码难以阅读,嵌套层级过深
- 错误处理分散在每一层
- 难以复用和测试
Promise 如何解决回调地狱
Promise 提供了链式调用,让异步代码可以写成扁平结构:
getData()
.then(a => getMoreData(a))
.then(b => getMoreData(b))
.then(c => getMoreData(c))
.then(d => console.log('done:', d))
.catch(err => console.error(err));
关键改进:
- 扁平结构:不再嵌套
- 统一错误处理:
.catch()捕获所有之前的错误 - 可复用:每个
.then()可以是独立的函数
async/await 的最终形态
async/await 让我们能用同步风格写异步代码:
async function fetchAll() {
try {
const a = await getData();
const b = await getMoreData(a);
const c = await getMoreData(b);
const d = await getMoreData(c);
console.log('done:', d);
} catch (err) {
console.error(err);
}
}
但这个写法有性能问题——每个 await 是串行的。如果它们之间没有依赖,应该并行执行:
async function fetchAllParallel() {
const [a, b, c, d] = await Promise.all([
getData(),
getMoreData(null), // 不需要前一步的结果
getMoreData(null),
getMoreData(null)
]);
console.log('done:', d);
}
常见陷阱
陷阱一:忘记 return Promise
// 错误:async2 不会等待 async1
async function wrong() {
async function async1() { /* ... */ }
async function async2() { /* ... */ }
async1();
async2();
}
// 正确
async function right() {
await async1();
await async2();
}
陷阱二:循环中的 await
// 错误:串行执行
async function wrong(urls) {
const results = [];
for (const url of urls) {
const result = await fetch(url);
results.push(result);
}
return results;
}
// 正确:并行执行
async function right(urls) {
const results = await Promise.all(urls.map(url => fetch(url)));
return results;
}
陷阱三:Promise.all 的部分失败
// 错误:一个失败全部失败
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
// 正确:用 allSettled 处理部分失败
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
错误处理的最佳实践
始终处理 rejection
// 错误:unhandled rejection
someAsyncOperation()
.then(result => process(result));
// 正确
someAsyncOperation()
.then(result => process(result))
.catch(error => console.error(error));
try/catch 在 async 函数中
async function fetchUser() {
try {
const response = await fetch('/api/user');
if (!response.ok) throw new Error('HTTP error');
return await response.json();
} catch (err) {
console.error('Fetch failed:', err);
throw err;
}
}
这一章想说的
Promise 和 async/await 大幅改善了 JavaScript 的异步编程体验,但引入了新的错误模式:
- 串行 vs 并行:没有依赖的异步操作应该并行执行
- 错误处理:每个 async 操作都应该处理 rejection
- 部分失败:用
Promise.allSettled而不是Promise.all处理可能部分失败的情况
写好异步代码的关键是:理解 Promise 的执行模型,知道什么时候用 await,什么时候用 Promise.all。