从"打爆电话"到"优雅等餐":Promise 与 async/await 的前世今生
你有没有经历过这样的代码?
getUserInfo(userId, function(user) {
getOrders(user.id, function(orders) {
getOrderDetails(orders[0].id, function(details) {
getPaymentInfo(details.paymentId, function(payment) {
// 恭喜你,已经分不清这是第几层了
console.log(payment);
});
});
});
});
这就是传说中的回调地狱——代码向右生长,像个倒过来的金字塔。
今天我们用一个故事,把 JavaScript 异步编程彻底讲明白。
一切从一个外卖订单开始
假设你在开发一个外卖 App,用户下单后,你的代码需要依次完成:
- 验证用户身份
- 检查餐厅是否营业
- 创建订单
- 扣款
- 通知骑手接单
每一步都要等上一步完成才能继续,而且每一步都可能失败。
这就是异步编程的经典场景:多个有依赖关系的异步操作串行执行。
原始时代:回调函数
打电话的模式
在 Promise 出现之前,我们用回调函数处理异步操作。就像打电话:
你:老板,给我做个汉堡!
老板:好的,做好了打你电话!
(你挂断电话,干别的事)
老板:(做好后)喂,汉堡好了!
代码实现:
function makeBurger(callback) {
setTimeout(() => {
callback(null, { name: '汉堡', price: 25 });
}, 2000);
}
makeBurger(function(err, burger) {
if (err) {
console.log('做失败了:', err);
return;
}
console.log('收到:', burger);
});
问题来了:连锁订单
如果要连续做汉堡、薯条、可乐呢?
makeBurger(function(err, burger) {
if (err) return handleError(err);
makeFries(function(err, fries) {
if (err) return handleError(err);
makeCola(function(err, cola) {
if (err) return handleError(err);
// 终于集齐了...
serveMeal([burger, fries, cola]);
});
});
});
问题显而易见:
- 代码向右"金字塔化"
- 错误处理到处重复
- 变量作用域混乱
- 很难在中间插入新步骤
Promise 时代:订单系统
什么是 Promise?
Promise 就像餐厅的取餐号牌:
你:老板,给我做个汉堡!
老板:给你个号牌,做好了叫号!
你:(拿着号牌走了)
号牌有三种状态:
- Pending(等待中):还没做好
- Fulfilled(已兑现):做好了,来取餐
- Rejected(已拒绝):卖光了,做不了
Promise 化的代码
function makeBurger() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 成功:交出汉堡
resolve({ name: '汉堡', price: 25 });
// 失败:拒绝订单
// reject('卖光了!');
}, 2000);
});
}
链式调用:优雅的流水线
makeBurger()
.then(burger => {
console.log('收到汉堡:', burger);
return makeFries();
})
.then(fries => {
console.log('收到薯条:', fries);
return makeCola();
})
.then(cola => {
console.log('收到可乐:', cola);
console.log('套餐齐了!');
})
.catch(err => {
// 统一的错误处理!
console.log('出问题了:', err);
});
对比回调地狱,代码变成了垂直的流水线:
- 每一步清晰可见
- 错误统一处理
- 随时可以插入新步骤
Promise.all:并行出餐
如果要同时做三个东西,谁先做好谁先上?
Promise.all([
makeBurger(),
makeFries(),
makeCola()
])
.then(([burger, fries, cola]) => {
// 三个都做好了,一起上桌
serveMeal([burger, fries, cola]);
})
.catch(err => {
// 任何一个失败,整个套餐取消
console.log('有东西做失败了:', err);
});
Promise.race:谁快用谁
点外卖时,同时看三家店,谁先接单用谁:
Promise.race([
orderFrom('麦当劳'),
orderFrom('肯德基'),
orderFrom('汉堡王')
])
.then(firstReady => {
console.log('最先接单的是:', firstReady);
});
async/await 时代:同步的错觉
终极优雅:像写同步代码一样写异步
Promise 链式调用已经很优雅了,但 async/await 把它推向了极致:
async function orderMeal() {
try {
const burger = await makeBurger();
console.log('收到汉堡:', burger);
const fries = await makeFries();
console.log('收到薯条:', fries);
const cola = await makeCola();
console.log('收到可乐:', cola);
console.log('套餐齐了!');
return [burger, fries, cola];
} catch (err) {
console.log('出问题了:', err);
}
}
看起来像同步代码,但实际上还是异步的!
底层原理:语法糖
async/await 本质上是 Promise 的语法糖。上面的代码等价于:
function orderMeal() {
return makeBurger()
.then(burger => {
console.log('收到汉堡:', burger);
return makeFries();
})
.then(fries => {
console.log('收到薯条:', fries);
return makeCola();
})
.then(cola => {
console.log('收到可乐:', cola);
console.log('套餐齐了!');
return [burger, fries, cola];
})
.catch(err => {
console.log('出问题了:', err);
});
}
async 函数自动返回 Promise
async function sayHello() {
return 'Hello!';
}
sayHello().then(msg => console.log(msg)); // 'Hello!'
await 只能在 async 函数内使用
// ❌ 错误!
const burger = await makeBurger();
// ✅ 正确
async function order() {
const burger = await makeBurger();
}
并行优化:不要过度 await
// ❌ 串行执行,慢!
async function slowOrder() {
const burger = await makeBurger(); // 等2秒
const fries = await makeFries(); // 等2秒
const cola = await makeCola(); // 等2秒
// 总共6秒
}
// ✅ 并行执行,快!
async function fastOrder() {
const [burger, fries, cola] = await Promise.all([
makeBurger(),
makeFries(),
makeCola()
]);
// 总共2秒(最慢的那个)
}
实战:完整的外卖订单流程
// 工具函数:模拟异步操作
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 各环节实现
async function verifyUser(userId) {
await delay(500);
if (!userId) throw new Error('用户未登录');
return { id: userId, name: '张三' };
}
async function checkRestaurant(restaurantId) {
await delay(300);
const isOpen = Math.random() > 0.2;
if (!isOpen) throw new Error('餐厅已打烊');
return { id: restaurantId, name: '麦当劳' };
}
async function createOrder(user, restaurant) {
await delay(800);
return {
orderId: 'ORD' + Date.now(),
userId: user.id,
restaurantId: restaurant.id
};
}
async function processPayment(order) {
await delay(1000);
const success = Math.random() > 0.1;
if (!success) throw new Error('支付失败');
return { ...order, paid: true };
}
async function notifyRider(order) {
await delay(500);
return { ...order, riderId: 'RIDER001', status: '配送中' };
}
// 主流程
async function placeOrder(userId, restaurantId) {
try {
console.log('开始处理订单...');
const [user, restaurant] = await Promise.all([
verifyUser(userId),
checkRestaurant(restaurantId)
]);
const order = await createOrder(user, restaurant);
const paidOrder = await processPayment(order);
const finalOrder = await notifyRider(paidOrder);
return finalOrder;
} catch (error) {
console.log('订单失败:', error.message);
throw error;
}
}
// 使用
placeOrder('USER001', 'REST001')
.then(order => console.log('订单完成:', order))
.catch(err => console.log('最终错误:', err));
总结:三代技术的选择
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 回调函数 | 简单、单层异步 | 理解简单 | 多层嵌套=地狱 |
| Promise | 复杂链式调用、并行操作 | 链式优雅、错误统一 | then 嵌套多了也乱 |
| async/await | 大部分异步场景 | 最优雅、最像同步 | 需要理解 Promise 原理 |
我的建议:
- 90% 场景用 async/await
- 需要并行时用 Promise.all
- 读别人的代码时,三者都要会
延伸:为什么 JavaScript 需要异步?
JavaScript 是单线程语言,一次只能做一件事。
如果用同步方式请求服务器:
const data = fetchSync('https://api.example.com/data');
// 在收到响应前,整个页面卡死!
所以 JavaScript 用异步:
fetch('https://api.example.com/data')
.then(data => console.log(data));
// 请求发出后,代码继续执行
最后的思考题
async function quiz() {
console.log(1);
await Promise.resolve().then(() => console.log(2));
console.log(3);
Promise.resolve().then(() => console.log(4));
console.log(5);
}
quiz();
输出顺序是什么?(答案在评论区揭晓)
记住:异步编程不是魔法,只是让代码在等待时"去做别的事"。理解了这一点,Promise 和 async/await 就不再是黑盒了。
下次写代码时,想象你在经营一家餐厅——让客人拿着号牌去干别的事,好了再叫号。
这就是异步编程的本质。
Views: 0
