太棒了!异步编程(尤其是 Promise)确实是前端开发中最核心也最容易出bug的知识点。别担心,我们完全可以把这块硬骨头啃下来。
既然你已经在写axios.get().then(res => res.data),我们就从这段代码出发,层层剥开Promise 的本质。
一、为什么要异步?(先解决“为什么”)
JavaScript 是单线程的语言(一次只能干一件事)。如果所有代码都“排队”执行(同步),遇到网络请求或定时器时,页面就会卡死(阻塞),直到请求结束。
一个绝佳的比喻:
- 同步(阻塞):你去食堂打饭,站在窗口前等着阿姨给你盛完,你再端着走。期间你什么都干不了。
- 异步(非阻塞):你去奶茶店点单,店员给你一张小票(Promise),说“做好了叫号”。你拿着小票,可以先去旁边逛逛街(执行其他代码),等听到叫号(请求完成),再回来取奶茶(执行回调)。
二、Promise 到底是什么?(解决“是什么”)
Promise 就是一个“小票”(或“契约”),它代表一个尚未完成但将来会完成的操作。它有三个“一生”的状态:
- Pending(待定/进行中):刚发起请求,后端还没回消息。(奶茶正在做)
- Fulfilled(已成功):请求成功,数据拿回来了。(奶茶做好了,叫号)
- Rejected(已失败):请求失败,网络断了或报错了。(机器坏了,做不了)
最核心的规则:状态一旦从 Pending 变成 Fulfilled 或 Rejected,就凝固了,不能再变。
三、拆解你的代码:.then()做了什么?
export const getUserList = (): Promise<User[]> => { return apiClient.get('/users').then(res => res.data); };这行代码的执行流程是这样的:
apiClient.get('/users'):立即返回一个Promise对象(状态为 Pending)。.then(res => res.data):.then()是 Promise 的原生方法,意为“等状态变成 Fulfilled(成功)后,执行这个函数”。- 当后端返回数据时,Promise 状态变为 Fulfilled,
res就是后端传回来的整个响应对象(包含状态码、请求头、数据等),然后我们箭头函数执行,把res.data(纯净的数据)取出来。
- 返回值:
.then()本身也会返回一个新的 Promise,所以外部调用时还能继续.then()。
灵魂拷问:如果不写
.then(res => res.data),调用方拿到的就是整个响应对象,需要写response.data才能拿到数据。你现在的写法相当于提前把数据剥出来了,非常优雅。
四、处理错误:.catch()和.finally()
现实世界请求一定会失败(后端报错、断网)。只写.then()会“静默失败”,页面卡死没反应。
完整的 Promise 链写法:
apiClient.get('/users') .then(res => { console.log('成功', res.data); return res.data; }) .catch(error => { // 🔥 只要请求失败,或者 .then 里抛出了异常,都会进这里 console.error('请求出错了!', error.message); // 可以给用户弹个 Toast 提示 return []; // 返回一个空数组,防止外面报错 }) .finally(() => { // 无论成功还是失败,最后都会执行(类似 Java 的 try-catch-finally) console.log('请求结束,关闭 Loading 动画'); });五、终极进阶:async/await(现代写法的“语法糖”)
上面的.then().catch()链式调用,一旦逻辑复杂起来会形成“回调地狱”(一大堆缩进)。ES7 推出了async/await,让异步代码长得像同步代码,极大提升可读性。
用async/await重写你的getUserList:
// 函数前面加 async export const getUserList = async (): Promise<User[]> => { try { // await 会“卡住”这一行,但只是假装卡住(实际上 JS 引擎去干别的活了), // 直到请求完成拿到结果,再赋值给 res const res = await apiClient.get('/users'); return res.data; // 这里的 return 相当于 Promise 的 resolve } catch (error) { // 相当于 .catch() console.error('请求失败', error); return []; // 异常时兜底 } finally { // 相当于 .finally() console.log('请求结束'); } };调用时:
// 因为 async 函数返回的一定是 Promise,所以调用时要么 await,要么 .then() const fetchData = async () => { const users = await getUserList(); // 成功拿到 User[] console.log(users[0].name); };六、高阶神器:并发请求(Promise.all和Promise.allSettled)
前端页面经常需要同时请求多个接口(比如首页要同时拿用户信息和公告)。如果你写await A(); await B();,那是串行,总耗时 = A耗时 + B耗时。
正确的“同时发起”做法:
Promise.all(全部成功才算成功)
适用场景:三个接口数据必须全部拿到才能渲染页面。const [users, posts, messages] = await Promise.all([ getUserList(), // 请求1 getPostList(), // 请求2 getMessageList() // 请求3 ]); // 三个请求同时发出,总耗时 ≈ 最慢的那个请求时间 // 如果其中任何一个失败,整个 Promise.all 会立即失败进入 catchPromise.allSettled(各自安好,互不影响)
适用场景:某个接口挂了不影响其他数据显示。const results = await Promise.allSettled([ getUserList(), getPostList() ]); // results 是一个数组,每个元素都有 status: 'fulfilled' 或 'rejected' // 你可以手动过滤掉失败的,只渲染成功的
七、一张图总结(建议收藏)
| 方法 | 作用 | 类比 |
|---|---|---|
new Promise((resolve, reject)=>{}) | 手动创建 Promise,调用resolve表示成功,reject表示失败 | 自己去奶茶店下订单 |
.then(res => {}) | 监听成功状态,拿到结果 | 奶茶叫号,你去取 |
.catch(err => {}) | 监听失败状态,捕获错误 | 奶茶做砸了,店员给你退款道歉 |
.finally(() => {}) | 无论成败,最后都执行 | 无论喝没喝到,你都走出奶茶店 |
async/await | 用同步风格写异步代码 | 像点菜一样写代码,但底层没阻塞 |
Promise.all([...]) | 并发请求,全部成功才成功(一荣俱荣) | 团购,一个人不付款订单就取消 |
Promise.allSettled([...]) | 并发请求,各自返回结果(各自安好) | 分别下单,谁成功拿谁 |
第八、给你的实战建议(避坑指南)
- 永远别忘记
catch(或try-catch):不捕获错误,前端页面可能会白屏(尤其是在 Vue/React 中)。 async函数返回值一定是 Promise:即使你写return 123,外面拿到的也是Promise.resolve(123)。- 在循环中不要乱用
await:如果你在for循环里await发请求,那是一个一个发的(慢)。如果需要同时发多个,用Promise.all配合.map。
// ❌ 错误:串行,耗时 3秒(假如每个1秒) for (let id of [1,2,3]) { await getUserById(id); } // ✅ 正确:并行,耗时 1秒 const promises = [1,2,3].map(id => getUserById(id)); await Promise.all(promises);异步编程刚接触时确实有点绕,但核心就是“将来完成 + 不阻塞”。把Promise想象成外卖订单号,把await想象成“坐在家里等外卖(但不影响你刷手机)”,就很好理解了。
如果你想把async/await结合你的 Vue 组件(比如onMounted里调用)实战练一下,或者想看看new Promise手动封装一个函数(比如延迟等待),随时告诉我,我们继续深挖!😊