Promise 像外卖订单
中午十二点,我点了份黄焖鸡。APP 显示"预计 30 分钟送达"——我没盯着屏幕等,而是继续敲代码。25 分钟后,手机响了:"您的外卖已放到前台"。这种"先下单,后取餐"的模式,和 Promise 一模一样。
说实话,刚开始学前端的时候,我对 Promise 这玩意儿是抵触的。那时候我觉得回调函数挺好用的啊,写起来直来直去,虽然嵌套多了确实眼晕,但凑合能用。直到三年前那个周五,我接了一个支付接口的活儿,才真正明白 Promise 为什么存在。
那是下午三点,产品经理老王走过来,拍了拍我的肩膀:"支付回调改一下,要先查用户,再查订单,再查订单详情,最后还要校验一下库存。"我嗯了一声,打开代码一看,差点没背过气去。
旧代码是这样的:
getUser(userId, (user) => {
getOrders(user.id, (orders) => {
getOrderDetail(orders[0].id, (detail) => {
console.log(detail);
});
});
});
三层回调,像俄罗斯套娃一样一层包一层。这还没完,老王又说:"每个环节都要加错误处理啊,支付这事儿不能马虎。"我试着加了几个 if (err),代码瞬间变成了金字塔形状——回调地狱这个词,我那天是真切体会到了。
改到下午六点,我眼睛都花了。屏幕上全是缩进,我像数牙签一样数着大括号。楼下烧烤摊的烟飘上来,孜然味混着空调房里的咖啡味,我肚子咕咕叫,但代码还没跑通。
这时候,我们组的后端老哥走了过来。他姓张,因为写的接口又稳又快,人称"张接口"。他端着保温杯,瞄了一眼我的屏幕,笑了:"你这代码,跟我刚入行时写的 SQL 嵌套有得一拼。"
我苦笑:"这回调嵌套太恶心了。"
他喝了口茶,问我:"你们前端不是有个叫 Promise 的东西吗?是不是跟我老婆网购差不多——下单时就知道会有结果,但不用一直盯着物流?"
我一愣,这比喻绝了。
张接口说得对,Promise 本质上就是一个"承诺"。
想象你去饭店吃饭,如果是普通的回调,你得站在柜台前等着,厨师喊一声"好了"你才能拿走。但 Promise 呢?你扫码下单,拿到一个订单号,然后就可以去找座位玩手机了。饭做好了自然会叫号,你不用盯着厨房看。
那个订单号,就是 Promise 对象。它代表了一个尚未完成但预期将来会完成的异步操作。
const order = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('黄焖鸡送到了');
}, 3000);
});
order.then(food => console.log(food));
这里的 resolve 就像是服务员喊"您的餐好了",then 就是你起身去取餐。在这 3 秒钟里,JavaScript 引擎不会被阻塞,该干嘛干嘛——渲染页面、响应用户点击、执行其他代码。
但外卖也有翻车的时候。万一下单后商家发现没米了,或者没有骑手接单,怎么办?
const order = new Promise((resolve, reject) => {
const hasRider = Math.random() > 0.5;
hasRider
? resolve('配送中')
: reject('暂无骑手接单');
});
order
.then(msg => console.log(msg))
.catch(err => console.log('退单:' + err));
reject 就是"这单做不了了",catch 就是你接到电话后处理退单。有了这套机制,错误处理从嵌套的最深层一下子浮到了表面,不用再一层一层往上抛了。
张接口看我似懂非懂,又补了一句:"你看啊,Promise 其实有三种状态——pending、fulfilled、rejected。就像外卖订单的'待接单''配送中''已送达'。一旦变成 fulfilled 或 rejected,就改不了了,这叫'不可变'。"
我点点头,这设计确实妙。状态确定了就是确定了,不会出现"既成功又失败"的诡异情况。
第二天,我开始用 Promise 重构那个支付接口。写着写着,发现新的问题——虽然解决了回调地狱,但如果要在 then 里面再发起请求,还是会嵌套。
张接口端着豆浆路过,指着我的屏幕说:"你这 then 里面再包 Promise,可以拍平了写。"
"拍平?"
"就是链式调用啊,return 一个新的 Promise,then 会自动等它完成。"
我恍然大悟,改成了这样:
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetail(orders[0].id))
.then(detail => console.log(detail))
.catch(err => console.log('出错了:', err));
从金字塔变成了一条链,像流水线一样顺。每个 then 接收上一个 then 的返回值,如果是 Promise,就自动等它 resolve。
更妙的是,每个环节还能加工数据,传给下一个:
fetchUser(1001)
.then(user => {
console.log('用户:', user.name);
return fetchOrders(user.id);
})
.then(orders => {
console.log('订单数:', orders.length);
return orders.filter(o => o.paid);
})
.then(paidOrders => console.log('已支付:', paidOrders));
那天雨很大,我 chain 完代码,发现窗外的梧桐叶被打落了一地。代码却意外地顺利跑通了,逻辑清晰得像那条笔直的雨线。
过了一周,要做个数据看板,需要同时调三个接口:用户信息、订单统计、库存预警。
我习惯性地开始写:
const user = await fetchUser();
const orders = await fetchOrders();
const inventory = await fetchInventory();
张接口正好路过,看了一眼:"你这样写,三个接口是串行的。fetchOrders 要等 fetchUser 回来才发请求,没必要啊。"
"那怎么办?"
"一起发啊, Promise.all,等最后一个回来就行了。"
const p1 = fetchUser();
const p2 = fetchOrders();
const p3 = fetchInventory();
Promise.all([p1, p2, p3])
.then(([user, orders, inventory]) => {
renderDashboard({ user, orders, inventory });
});
三个请求一起走,像三辆外卖车同时从不同的商家出发。Promise.all 会等最后一辆车到齐,才把三份餐一起端给你。
但张接口提醒我:"有个坑啊,Promise.all 是'一损俱损'的。"
Promise.all([
Promise.resolve('A'),
Promise.reject('B 出错了'),
Promise.resolve('C')
])
.then(console.log)
.catch(err => console.log(err)); // "B 出错了"
有一个退单,整单取消。只要有一个 Promise 被 reject,整个 Promise.all 就失败了,拿不到任何结果。这跟现实中点外卖还真不一样——现实中一份餐没了,其他的还能吃。
后来我查文档,发现 ES2020 出了个 Promise.allSettled,可以等全部完成,不管成功失败。但在那会儿,我们只能用 Promise.all,然后小心翼翼地处理错误。
又过了一阵子,我在做图片加载优化。页面上有几张大图,从主站 CDN 加载有时候慢,我们备了两个 CDN 源。
我跟张接口说:"我想两个 CDN 同时请求,哪个快用哪个。"
他说:"这就是 race,竞速。"
const cdn1 = fetch('https://cdn-a.com/img.png');
const cdn2 = fetch('https://cdn-b.com/img.png');
Promise.race([cdn1, cdn2])
.then(img => showImage(img));
两个请求赛跑,谁先回来用谁,另一个自动放弃。这就像你同时点了两个外卖,谁先到你吃谁——虽然现实中这么做有点缺德,但在代码里这可是正经的优化手段。
这个特性还常被用来封装请求超时:
const timeout = new Promise((_, reject) => {
setTimeout(() => reject('请求超时'), 5000);
});
Promise.race([fetchData(), timeout])
.then(data => console.log(data))
.catch(err => console.log(err));
给请求加个 5 秒倒计时,超时算输。这招在弱网环境下特别好用,避免用户盯着空白页面发呆。
张接口看了我的代码,满意地点点头:"会用 race 了,进阶了。"
Promise 链写多了,总觉得还是不够优雅。那一串 .then().then().then() 虽然比回调地狱强,但看着还是像链条,不够自然。
ES2017 出了 async/await,张接口兴奋地跑过来:"这是语法糖,但糖甜啊!"
async function loadData() {
try {
const user = await getUser(1001);
const orders = await getOrders(user.id);
const detail = await getOrderDetail(orders[0].id);
return detail;
} catch (err) {
console.log('出错了:', err);
}
}
看起来像同步代码,一行一行往下走,但实际上每一步都是异步的。await 会暂停函数执行,等 Promise resolve 了再继续,而不会阻塞主线程。
这种写法最大的好处是——可以用 try/catch 处理异步错误了。以前 Promise 链的错误处理要么用 catch,要么在每个 then 里写错误回调,现在直接一个 try/catch 包起来,和写同步代码一样自然。
但这里有个坑要注意:await 会阻塞后续代码。如果你这样写:
async function loadSlow() {
const user = await fetchUser(); // 等 1 秒
const orders = await fetchOrders(); // 再等 1 秒
const inventory = await fetchInventory(); // 又等 1 秒
return { user, orders, inventory };
}
三个串行请求,总共要等 3 秒。想并行还得配合 Promise.all:
async function loadDashboard() {
const [user, orders, inventory] = await Promise.all([
fetchUser(),
fetchOrders(),
fetchInventory()
]);
return { user, orders, inventory };
}
await 配合 Promise.all,既保留了代码的简洁性,又不损失性能。张接口说这就是"既要又要",语法糖和效率两不误。
代码写完,天已经黑了。我揉揉脖子,想起中午点的黄焖鸡早就凉透了。
张接口收拾包准备走,路过我工位时说:"现在明白 Promise 了吧?其实就是个思维方式——不要等着,要等着的时候告诉我,好了我再处理。"
我点点头。这不仅是代码的哲学,也挺像人生很多事—— submit 了一份简历,发起了一个请求,然后继续过自己的生活,等 HR 的回调。
现在每次看到外卖 APP 上的"配送中",我都会想起 Promise。那是一份承诺——饭会来,结果会来,只是需要等一等。而在等待的时间里,CPU 不会空转,页面不会卡住,其他的代码该跑跑,该渲染渲染。
异步的美妙就在于此。
门外传来脚步声,由远及近,停在了门口。是骑手吗?
#前端 #JavaScript #技术生活 #编程感悟 #工程师日常