前端新人必读:彻底搞懂Promise与async/await异步编程(附实战技
去年冬天,我陪一个实习生小哥调 bug,场景特简单:页面一加载就要调两个接口,拿到用户信息和广告配置,再把用户名塞进广告标题里。Promise 和 async/await 就是刚需里的瑞士军刀——不会用,你就永远在“等”:等接口、等时机、等 bug 自己长脚跑掉。你找后端借数据,后端说:“先给你张欠条,到时候要么给你数据(resolve),要么告诉你没戏(reject)。理解这个顺序,你就明白为什

前端新人必读:彻底搞懂Promise与async/await异步编程(附实战技
- 前端新人必读:彻底搞懂Promise与async/await异步编程(附实战技巧)
-
- 引言:为什么你的代码总在“等”?
- JavaScript 异步世界的入场券:认识 Promise 对象
- Promise 的三种状态:pending、fulfilled 和 rejected 到底意味着什么
- 从回调地狱到优雅链式调用:Promise 的基本使用姿势
- 手把手教你创建和组合多个 Promise:resolve、reject、all、race 全解析
- async/await 是什么?它和 Promise 有什么关系?
- 用 async/await 写出同步风格的异步代码:真实项目中的写法示范
- Promise 与 async/await 各自的优缺点:什么时候该用谁?
- 常见陷阱大揭秘:未捕获的 reject、忘记 return、滥用 await 等典型问题
- 调试异步代码不再头疼:浏览器 DevTools 中如何追踪 Promise 执行流
- 提升开发效率的小技巧:封装通用请求函数、统一错误处理、避免嵌套过深
- 当你遇到“Promise 不执行”或“await 没反应”时,排查思路是什么?
- 让代码更健壮:如何优雅地处理超时、重试和并发限制
- 异步编程不只是语法:理解事件循环对 Promise 执行顺序的影响
- 别再被面试官问倒:几个高频 Promise 面试题背后的原理剖析
- 写异步代码也能有仪式感:团队协作中推荐的 Promise 编码规范
- 尾声:把异步代码写得像诗,把头发留在头上
前端新人必读:彻底搞懂Promise与async/await异步编程(附实战技巧)
引言:为什么你的代码总在“等”?
先讲个真事。
去年冬天,我陪一个实习生小哥调 bug,场景特简单:页面一加载就要调两个接口,拿到用户信息和广告配置,再把用户名塞进广告标题里。结果他写了 60 多行 callback,嵌套七层,最后变量名都写成 data1、data2、data3……打印出来能当楼梯。
我递给他一杯速溶咖啡,说:“兄弟,你知道自己为什么脱发吗?因为 callback 在薅你头发。”
他苦着脸:“那咋办?”
我:“把 callback 扔进垃圾桶,让 Promise 和 async/await 给你植发。”
两小时后,代码从 60 行缩到 12 行, bug 没了,发量虽然没长回来,但至少不再继续秃。
这个故事每天都在前端圈子里上演。
JavaScript 是单线程的,却偏要干多线程的活儿:接口请求、定时器、文件读写、事件监听……于是“异步”成了刚需。
Promise 和 async/await 就是刚需里的瑞士军刀——不会用,你就永远在“等”:等接口、等时机、等 bug 自己长脚跑掉。
今天这篇,把我过去五年在异步坑里摔过的跤、流过的泪、写过的 TODO 注释,一次性打包给你。
读完你要是还说不会,我把键盘吃了——机械轴的,嘎嘣脆。
JavaScript 异步世界的入场券:认识 Promise 对象
Promise 是啥?
官方话术:Promise 是表示异步操作最终完成或失败的对象。
人话:它是一张“欠条”。你找后端借数据,后端说:“先给你张欠条,到时候要么给你数据(resolve),要么告诉你没戏(reject)。”
你拿着欠条,就能提前安排“成功后干啥”、“失败后干啥”,而不是傻站在门口等到海枯石烂。
// 一张最简陋的欠条
const欠条 = new Promise((resolve, reject) => {
// 模拟借钱过程
setTimeout(() => {
const 钱到账 = Math.random() > 0.3; // 70% 概率成功
if (钱到账) {
resolve('5000块'); // 钱到位,把数据传下去
} else {
reject('余额不足'); // 钱没到位,把原因传下去
}
}, 1000);
});
// 拿着欠条安排后续
欠条
.then((现金) => console.log('请小姐姐喝奶茶,花了', 现金))
.catch((理由) => console.log('小姐姐说:', 理由));
上面这段代码,如果你之前只玩过 callback,会突然有种“世界亮了”的感觉:
- 没有嵌套
- 错误集中处理
- 链式调用像写诗
但别急着嗨,Promise 的坑都在细节里,下面咱们一点点拆。
Promise 的三种状态:pending、fulfilled 和 rejected 到底意味着什么
Promise 的一生只有三种状态,像人的感情:
- pending:暧昧期,啥都没发生
- fulfilled:表白成功,对象答应你了
- rejected:表白失败,对方把你拉黑了
关键规则:
- 状态一旦改变,就不可逆,像极了爱情
- 你在 then 里能拿到成功的“情书”,在 catch 里拿到失败的“好人卡”
- 如果你既不写 then 也不写 catch,Promise 会把你当渣男,抛一个
Uncaught (in promise)报错,浏览器控制台红得发紫
const 心情 = new Promise((resolve, reject) => {
// 模拟表白
setTimeout(() => {
resolve('❤️ 我也喜欢你');
// 如果写 reject('你是个好人') 就会走向另一个结局
}, 500);
});
console.log('0ms:', 心情); // pending
心情
.then((回信) => console.log('500ms:', 回信))
.catch((发卡) => console.log('500ms:', 发卡));
// 输出顺序:
// 0ms:Promise {<pending>}
// 500ms:❤️ 我也喜欢你
记住:状态变化只跟 resolve/reject 有关,跟你写多少 then 无关。
很多新人以为多写几个 then 就能多收几次结果,其实只能收到一次——Promise 不是复读机。
从回调地狱到优雅链式调用:Promise 的基本使用姿势
先给你看一眼“地狱”长啥样:
// callback 地狱,慎入
getUserId(function (id) {
getUserInfo(id, function (info) {
getOrders(info.uid, function (orders) {
getDetail(orders[0].orderId, function (detail) {
console.log(detail); // 五层嵌套,眼睛已瞎
});
});
});
});
再给你看一眼“天堂”:
// Promise 链式,像剥洋葱,一层一层又平又直
getUserId()
.then(id => getUserInfo(id))
.then(info => getOrders(info.uid))
.then(orders => getDetail(orders[0].orderId))
.then(detail => console.log(detail))
.catch(err => console.error('出错了', err));
链式调用的秘诀:
- 每个 then 里 return 一个新的 Promise,就能把数据继续往下传
- 任何一个环节报错,都会被最近的 catch 捕获,不用每层都写 if-err
- 如果忘了 return,下一层的 then 会收到 undefined,这种 bug 藏得深, Debug 时想跳楼
// 错误示例:忘了 return
getUserId()
.then(id => {
getUserInfo(id); // 没 return,后续拿到的是 undefined
})
.then(info => console.log(info)); // undefined
手把手教你创建和组合多个 Promise:resolve、reject、all、race 全解析
- 快速创建已决 Promise
Promise.resolve(3); // 直接成功,值为 3
Promise.reject('错了'); // 直接失败,原因为 '错了'
- Promise.all——“全部到齐才开饭”
const 任务1 = fetch('/api/user');
const 任务2 = fetch('/api/config');
const 任务3 = fetch('/api/ad');
Promise.all([任务1, 任务2, 任务3])
.then(([res1, res2, res3]) => Promise.all([res1.json(), res2.json(), res3.json()]))
.then(([user, config, ad]) => {
console.log('人到齐,开饭:', user, config, ad);
})
.catch(err => console.log('有一个人没到,饭局取消', err));
特点:
- 所有 Promise 都成功,才算成功,返回值按顺序排列
- 只要有一个失败,整体就失败,失败原因是第一个失败的那个
- Promise.race——“谁先到就吃谁”
const 超时包装 = new Promise((resolve, reject) => {
setTimeout(() => reject('超时啦'), 5000);
});
Promise.race([fetch('/api/slow'), 超时包装])
.then(res => res.json())
.then(data => console.log('成功拿到数据:', data))
.catch(err => console.log('要么接口跪,要么超时', err));
race 经典用法:给接口加超时控制,谁先跑完就认谁。
- Promise.allSettled——“一个都不能少,结果我全都要”
Promise.allSettled([Promise.resolve(1), Promise.reject('错'), Promise.resolve(3)])
.then(results => {
results.forEach((r, idx) =>
r.status === 'fulfilled'
? console.log(`任务${idx}成功`, r.value)
: console.log(`任务${idx}失败`, r.reason)
);
});
allSettled 无论成功失败都会等你,适合“批量上报”场景。
async/await 是什么?它和 Promise 有什么关系?
一句话:async/await 是 Promise 的语法糖,让你用同步的写法写异步。
async 函数默认返回一个 Promise,await 会暂停线程(只是看起来像暂停,实际事件循环还在跑)直到 Promise 解决。
// 把之前的链式改写成 async/await,阅读体验直线上升
async function 加载详情() {
try {
const id = await getUserId();
const info = await getUserInfo(id);
const orders = await getOrders(info.uid);
const detail = await getDetail(orders[0].orderId);
console.log(detail);
} catch (err) {
console.error('出错了', err);
}
}
注意细节:
- await 只能写在 async 函数里,否则会报语法错误
- await 后面如果不是 Promise,也会自动用 Promise.resolve 包装
- try/catch 既能捕获同步异常,也能捕获 await 后的 reject,堪称“一键兜底”
用 async/await 写出同步风格的异步代码:真实项目中的写法示范
- 统一封装请求函数
// 基于 fetch 封装,自带超时、错误码处理、token 注入
async function request(url, options = {}, timeout = 8000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const mergeOpts = {
...options,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.token}`,
...options.headers,
},
signal: controller.signal,
};
try {
const res = await fetch(url, mergeOpts);
clearTimeout(id);
if (!res.ok) {
// 把 HTTP 错误统一包装成 Error 对象
throw new Error(`请求失败 ${res.status}`);
}
return await res.json();
} catch (err) {
// 网络错误、超时、abort 全走到这里
console.error('request 错误:', err);
throw err; // 继续向上抛,让业务层决定要不要处理
}
}
- 业务层像写同步逻辑一样调用
async function 初始化首页() {
try {
// 三个请求无依赖,并发拉取
const [user, config, ad] = await Promise.all([
request('/api/user'),
request('/api/config'),
request('/api/ad'),
]);
renderUser(user);
renderConfig(config);
renderAd(ad);
} catch (e) {
showToast('首页数据加载失败,请刷新');
}
}
- 顺序依赖的场景
async function 提交订单() {
// 1. 先创建订单
const order = await request('/api/order/create', { method: 'POST', body: JSON.stringify(cart) });
// 2. 再获取支付参数
const payParams = await request('/api/pay/sign', { method: 'POST', body: JSON.stringify({ orderId: order.id }) });
// 3. 调起收银台
await wechatPay(payParams);
}
Promise 与 async/await 各自的优缺点:什么时候该用谁?
| 场景 | Promise 链 | async/await |
|---|---|---|
| 简单并发 | Promise.all 一把梭 | 需要手动 Promise.all |
| 顺序依赖 | 链式 then 可读性略差 | await 顺序写,直观 |
| 错误处理 | 只能 catch 一次,有时需要多次 | try/catch 想包多少就多少 |
| 循环+异步 | 需要递归或 reduce 写复杂 | for…of + await 真香 |
| 浏览器兼容 | IE 全灭,需 polyfill | 同样 IE 全灭,需 regenerator |
| 调试 | 堆栈浅,断点跳来跳去 | 堆栈深,但 DevTools 已能正确映射 |
结论:
- 80% 业务代码用 async/await,阅读成本低,后期维护省头发
- 需要复杂并发控制、竞态、超时组合,适当穿插 Promise.all/race
- 库作者封装底层 API 时,返回 Promise 更纯粹,不给用户强加 async 语法
常见陷阱大揭秘:未捕获的 reject、忘记 return、滥用 await 等典型问题
- 未捕获 reject
Promise.reject('💥'); // 控制台一片红
// 解决:始终写 catch,或者全局监听
window.addEventListener('unhandledrejection', e => {
console.error('全局抓到未处理的 reject', e.reason);
});
- 忘记 return,导致后续收到空气
async function wrapper() {
const res = await fetch('/api');
// 忘记 return res.json()
}
wrapper().then(data => console.log(data)); // undefined
- 滥用 await,把并发串成顺序
// 错误:本来可以一起跑,却被 await 串成糖葫芦
const user = await request('/user');
const config = await request('/config'); // 白白多等 200ms
- 在循环里直接 push Promise,没等结果
const arr = [];
for (const id of ids) {
arr.push(request(`/item/${id}`)); // ✅ 正确,先塞 Promise
}
const results = await Promise.all(arr);
- try/catch 只包一行,导致逻辑中断
try {
const data = await request('/pay');
} catch (e) {
console.log('支付失败');
}
// 如果支付失败,后面还会继续执行,可能产生脏数据
// 解决:把后续依赖逻辑全放进 try,或 catch 里 return 提前中断
调试异步代码不再头疼:浏览器 DevTools 中如何追踪 Promise 执行流
- 断点打在 await 语句上,DevTools 会停住,本地变量能看到 Promise 状态
- Console 里直接输入
Promise.resolve(1)会立即打印,方便做实验 - Network 面板筛选 XHR/fetch,查看真实耗时,配合 race 做超时验证
- Performance 面板录制,可以看到微任务(Micro Task)的执行阶梯,理解“事件循环”里 Promise 为什么比 setTimeout 先跑
小技巧:
给关键 Promise 加标记,方便堆栈追踪
const 标记 = (p, name) => {
p.catch(() => {}); // 避免未捕获
return p.finally(() => console.log(`[${name}] 完成`));
};
标记(request('/api/user'), '用户接口');
提升开发效率的小技巧:封装通用请求函数、统一错误处理、避免嵌套过深
- 业务错误码也抛 Error,保持风格统一
async function requestWithCode(url, options) {
const res = await request(url, options);
if (res.code !== 0) {
const err = new Error(res.message);
err.code = res.code;
throw err;
}
return res.data;
}
- 自动重试装饰器
function retry(fn, times = 3) {
return async function (...args) {
let lastErr;
for (let i = 0; i < times; i++) {
try {
return await fn(...args);
} catch (e) {
lastErr = e;
console.log(`第${i + 1}次重试`);
}
}
throw lastErr;
};
}
const robustRequest = retry(request, 3);
- 并发限流
class Semaphore {
constructor(max) {
this.max = max;
this.curr = 0;
this.queue = [];
}
async acquire() {
if (this.curr >= this.max) {
await new Promise(resolve => this.queue.push(resolve));
}
this.curr++;
}
release() {
this.curr--;
const resolve = this.queue.shift();
if (resolve) resolve();
}
}
const sem = new Semaphore(5); // 同时只允许 5 个并发
async function limitedRequest(url) {
await sem.acquire();
try {
return await request(url);
} finally {
sem.release();
}
}
当你遇到“Promise 不执行”或“await 没反应”时,排查思路是什么?
- 先看函数有没有被调用
- 再看函数体是不是 async,await 只能活在 async 里
- 在 await 前面 console.log 一把,确认程序跑到这里没
- 检查是不是早被 reject,但你没写 catch,异常吞掉
- 确认是不是死循环阻塞了线程,导致微任务队列永远不被清空
- 用上文提到的全局
unhandledrejection监听,把隐匿错误揪出来
让代码更健壮:如何优雅地处理超时、重试和并发限制
把上面 retry、race、Semaphore 组合起来,就能得到一个“能打”的请求模块:
- 超时用 race
- 失败用 retry
- 并发用信号量
async function superRequest(url) {
const req = () => request(url);
const timeout = (t) => new Promise((_, reject) => setTimeout(() => reject('超时'), t));
const robust = retry(req, 3);
return Promise.race([robust(), timeout(8000)]);
}
// 业务层再控制并发
const urls = ['a', 'b', 'c', 'd', 'e'];
await Promise.all(urls.map(u => limitedRequest(u)));
异步编程不只是语法:理解事件循环对 Promise 执行顺序的影响
来看一道经典面试题:
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
输出顺序:1 → 4 → 3 → 2
原因:
- 同步代码最先跑,所以 1、4 马上打印
- Promise.then 属于微任务(MicroTask),在当前宏任务结束后立即全部清空
- setTimeout 属于宏任务,哪怕延时 0,也要等下一轮事件循环
理解这个顺序,你就明白为什么“await 后面的代码”总是在“当前函数体”之后、却又在“setTimeout”之前执行。
掌握事件循环,才能真正把异步代码写得“可控”,而不是靠蒙。
别再被面试官问倒:几个高频 Promise 面试题背后的原理剖析
-
Promise.resolve(1).then(() => 2).then(console.log)打印啥?
答:2。因为 then 的返回值会包装成 Promise 继续往下传。 -
如何让
Promise.all在单个失败时依然跑完全部?
答:用 allSettled,或者提前在每个 Promise 里 catch 掉错误,返回标记值。 -
手写一个 Promise.all(简化版)
function myAll(promises) {
return new Promise((resolve, reject) => {
const ret = [];
let count = 0;
promises.forEach((p, idx) => {
Promise.resolve(p)
.then(data => {
ret[idx] = data;
if (++count === promises.length) resolve(ret);
})
.catch(reject);
});
});
}
-
async 函数里 return 1,外部得到的是?
答:Promise(1)。async 永远返回 Promise。 -
如何实现 Promise 的取消?
原生 Promise 不支持取消,需要借助第三方库(如 bluebird)或 AbortController。业务层最好封装“可取消”的高阶函数,避免内存泄漏。
写异步代码也能有仪式感:团队协作中推荐的 Promise 编码规范
- 统一返回 Promise:所有异步函数都返回 Promise,别让同事猜
- 错误信息标准化:抛 Error 对象,带 code、message、detail,方便日志上报
- catch 职责单一:底层只抛不处理,业务层集中 toast/上报
- 并发必须加评论:为什么用 all、为什么用 race,让后人看懂
- 拒绝“裸 await”:公共库内部如果不需要顺序,用 Promise.all 提速
- 注释里写清依赖顺序:尤其是链式调用,标好“1. 获取 token 2. 拉订单 3. 刷新本地缓存”
- Code Review 红线:看见嵌套 then 直接打回;看见没有 catch 直接打回;看见 forEach 里 await 直接打回
尾声:把异步代码写得像诗,把头发留在头上
Promise 和 async/await 不是“新语法”,而是“新生活方式”。
它们让你把“等”的焦虑,变成“掌控”的从容;
让你把嵌套的迷宫,拉成一条直路;
让你把凌晨两点的 console,变成十点准时的关机。
当然,工具再好,也架不住滥用。
记住三句话:
- 并发别串行,头发会喊疼
- 错误要兜底,不然背锅到白头
- 注释写清楚,后人少秃头
愿你在未来的代码里,
不再被回调薅头发,
不再被未捕获 reject 吓破胆,
不再被“为什么接口又超时”折磨到失眠。
愿你写完最后一行 await,
安心合上电脑,
去拥抱夜色,
以及——
尚未秃头的自己。
欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!
| 专栏系列(点击解锁) | 学习路线(点击解锁) | 知识定位 |
|---|---|---|
| 《微信小程序相关博客》 | 持续更新中~ | 结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等 |
| 《AIGC相关博客》 | 持续更新中~ | AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结 |
| 《HTML网站开发相关》 | 《前端基础入门三大核心之html相关博客》 | 前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识 |
| 《前端基础入门三大核心之JS相关博客》 | 前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。 通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心 |
|
| 《前端基础入门三大核心之CSS相关博客》 | 介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页 | |
| 《canvas绘图相关博客》 | Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化 | |
| 《Vue实战相关博客》 | 持续更新中~ | 详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅 |
| 《python相关博客》 | 持续更新中~ | Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具 |
| 《sql数据库相关博客》 | 持续更新中~ | SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能 |
| 《算法系列相关博客》 | 持续更新中~ | 算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维 |
| 《IT信息技术相关博客》 | 持续更新中~ | 作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识 |
| 《信息化人员基础技能知识相关博客》 | 无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方 | |
| 《信息化技能面试宝典相关博客》 | 涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面 | |
| 《前端开发习惯与小技巧相关博客》 | 持续更新中~ | 罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等 |
| 《photoshop相关博客》 | 持续更新中~ | 基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结 |
| 日常开发&办公&生产【实用工具】分享相关博客》 | 持续更新中~ | 分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具 |
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

更多推荐


所有评论(0)