同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端正则干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
写了那么久前端,接口请求写了那么多次——但每次碰到「先查 A 再根据 A 的结果查 B」还是习惯一层层回调?
Promise.all和Promise.allSettled到底该用哪个?错误该在哪里统一处理?loading 开着关不上、重复请求怎么防?这篇文章不讲事件循环底层,就一个目标:让你看完以后,接口请求能手写、能选对方案、能少踩坑。
一、异步到底在解决什么问题
一句话定义: 异步是为了在「等待耗时操作(如接口请求)」时,不阻塞主线程,让页面继续响应用户操作。
| 场景 | 你在干什么 |
|---|---|
| 用户点击按钮发起登录请求 | 等接口返回,再跳转或提示 |
| 页面加载时拉取用户信息 + 列表数据 | 多个接口并行请求,等全部完成再渲染 |
| 先查省市区,再根据选中的省查市 | 串行请求,后者依赖前者 |
| 上传文件时显示进度条 | 监听 xhr 或 fetch 的进度事件 |
为什么不用同步? 因为 JS 单线程,同步等接口会卡住整个页面。所以需要「发起请求 → 去做别的事 → 请求好了再回来处理」。
二、Promise 基础:搞清楚「状态」和「链式」
2.1 三种状态
pending(进行中) → fulfilled(成功) 或 rejected(失败)
状态一旦变更就不能再改。then 只会执行一次(成功走第一个回调,失败走第二个或 catch)。
2.2 链式调用的本质
fetch('/api/user')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
每一层 then 都返回一个新的 Promise。如果回调里 return 了值,下一个 then 会拿到这个值;如果 return 了 Promise,下一个 then 会等这个 Promise 完成。
常见误区: 以为 catch 只接上面那一层 then 的错误。其实 catch 会捕获前面整条链上未处理的 rejection。
fetch('/api/user')
.then(res => res.json()) // 这里抛错
.then(data => console.log(data))
.catch(err => console.error(err)); // 也会被这里捕获
三、async/await 的正确打开方式
3.1 本质:语法糖
async 函数一定返回 Promise;await 会暂停函数执行,等到 Promise 完成再继续。
async function getUser() {
const res = await fetch('/api/user');
const data = await res.json();
return data;
}
// 等价于
function getUser() {
return fetch('/api/user')
.then(res => res.json());
}
3.2 必须加 try-catch
async function loadData() {
try {
const res = await fetch('/api/data');
const data = await res.json();
return data;
} catch (err) {
console.error('请求失败', err);
throw err; // 需要让调用方知道失败了就 throw
}
}
不写 try-catch 会怎样? 错误会变成「未处理的 Promise rejection」,不会进 catch,调试和监控都不友好。
3.3 await 必须在 async 函数里
// ❌ 报错
function init() {
const data = await fetch('/api/data'); // SyntaxError
}
// ✅ 正确
async function init() {
const data = await fetch('/api/data');
}
四、接口请求中的串行与并行
这是日常最容易搞混也最容易写错的地方。
4.1 串行请求:后者依赖前者
// 需求:先查用户信息,再根据 userId 查订单列表
async function loadUserAndOrders() {
const userRes = await fetch('/api/user');
const user = await userRes.json();
const ordersRes = await fetch(`/api/orders?userId=${user.id}`);
const orders = await ordersRes.json();
return { user, orders };
}
特点:一个一个等,总耗时 = 各接口耗时之和。适合「B 依赖 A 的结果」的场景。
4.2 并行请求:互不依赖,一起发
// 需求:用户信息和配置可以同时拉
async function loadPageData() {
const [userRes, configRes] = await Promise.all([
fetch('/api/user'),
fetch('/api/config')
]);
const [user, config] = await Promise.all([
userRes.json(),
configRes.json()
]);
return { user, config };
}
用 Promise.all 的原因: 同时发请求,等最慢的那个完成。总耗时 ≈ 最慢接口的耗时,而不是两者相加。
4.3 并行:all 和 allSettled 怎么选?
| 方法 | 行为 | 适用场景 |
|---|---|---|
Promise.all | 有一个失败就整体 reject | 多个接口都成功才算成功 |
Promise.allSettled | 全部执行完,返回每个的结果/错误 | 部分失败也要拿到成功的数据 |
// 三个接口,有一个失败就整体失败
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
// 三个接口,要拿到所有结果(含失败原因)
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
results.forEach((r, i) => {
if (r.status === 'fulfilled') console.log(`接口${i}成功`, r.value);
else console.log(`接口${i}失败`, r.reason);
});
4.4 常见写法对比
// ❌ 串行写法,却当成并行在用——浪费时间
async function loadData() {
const a = await fetchA(); // 等 A 完
const b = await fetchB(); // 再等 B
const c = await fetchC(); // 再等 C
return { a, b, c };
}
// ✅ 并行
async function loadData() {
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
return { a, b, c };
}
五、错误统一处理
5.1 单个请求的 try-catch
async function request(url, options = {}) {
try {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
} catch (err) {
console.error('请求失败', err);
throw err;
}
}
5.2 封装一层统一的请求函数
async function request(url, options = {}) {
const res = await fetch(url, {
...options,
headers: { 'Content-Type': 'application/json', ...options.headers }
});
const data = await res.json();
if (data.code !== 0) {
throw new Error(data.message || '请求失败');
}
return data.data;
}
业务层只关心「成功拿到 data」或「抛错」,错误格式统一。
5.3 全局错误处理(如 axios 拦截器思路)
// 以 fetch 封装为例
async function request(url, options = {}) {
const res = await fetch(url, options);
const data = await res.json();
if (!res.ok) {
// 401 统一跳登录
if (res.status === 401) {
window.location.href = '/login';
throw new Error('未登录');
}
// 其他错误统一提示
message.error(data.message || '请求失败');
throw new Error(data.message);
}
return data;
}
5.4 并行时的错误处理
// Promise.all:一个失败全部失败
try {
const [a, b] = await Promise.all([fetchA(), fetchB()]);
} catch (err) {
// A 或 B 任意一个失败都会进这里
message.error('加载失败');
}
// Promise.allSettled:分别处理
const results = await Promise.allSettled([fetchA(), fetchB()]);
const errors = results.filter(r => r.status === 'rejected');
if (errors.length) {
message.error(`${errors.length} 个请求失败`);
}
六、Loading 状态控制
6.1 基本写法:先开再关
async function handleSubmit() {
setLoading(true);
try {
await submitForm();
message.success('提交成功');
} catch (err) {
message.error('提交失败');
} finally {
setLoading(false); // 无论成功失败都要关
}
}
关键: 用 finally 保证 loading 一定会被关掉,避免「点了没反应」的假象。
6.2 多个请求时的 loading
async function loadData() {
setLoading(true);
try {
const [user, list] = await Promise.all([
fetchUser(),
fetchList()
]);
setUser(user);
setList(list);
} catch (err) {
message.error('加载失败');
} finally {
setLoading(false);
}
}
一次开、一次关即可,不用每个请求单独控制。
6.3 防止重复提交:用标志位
function SubmitButton() {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
if (loading) return; // 防重复点击
setLoading(true);
try {
await submit();
message.success('成功');
} catch (err) {
message.error('失败');
} finally {
setLoading(false);
}
};
return <Button loading={loading} onClick={handleClick}>提交</Button>;
}
6.4 防重复请求:AbortController
let controller = null;
async function search(keyword) {
controller?.abort(); // 取消上一次
controller = new AbortController();
try {
const res = await fetch(`/api/search?q=${keyword}`, {
signal: controller.signal
});
return res.json();
} catch (err) {
if (err.name === 'AbortError') return; // 被取消,不处理
throw err;
}
}
七、真实踩坑案例
坑 1:for 循环里 await 写成串行
// ❌ 本意是并行,实际是串行
const ids = [1, 2, 3];
const results = [];
for (const id of ids) {
const res = await fetch(`/api/item/${id}`);
results.push(await res.json());
}
// ✅ 真正并行
const results = await Promise.all(
ids.map(id => fetch(`/api/item/${id}`).then(r => r.json()))
);
坑 2:忘记在 catch 里重新 throw
async function load() {
try {
return await fetchData();
} catch (err) {
message.error('加载失败');
// 这里没 throw,调用方拿不到失败信息,会当成功处理
}
}
// 调用方
const data = await load(); // 失败时 data 是 undefined,容易出 bug
需要让上层知道失败时,记得 throw err。
坑 3:React 中请求完成时组件已卸载
useEffect(() => {
let cancelled = false;
async function load() {
const data = await fetchData();
if (!cancelled) setData(data);
}
load();
return () => { cancelled = true; };
}, []);
坑 4:接口返回的「业务错误」没当错误处理
// 接口返回 { code: 400, message: '参数错误' },但 HTTP 200
const data = await res.json();
if (data.code !== 0) {
throw new Error(data.message); // 要主动 throw,否则后面逻辑会出错
}
return data.data;
八、实战最佳实践总结
| 维度 | 建议 |
|---|---|
| 串行 vs 并行 | 无依赖用 Promise.all 并行;有依赖就老老实实串行 await |
| 错误处理 | async 函数内必有 try-catch;统一封装可放在 request 层 |
| Loading | 用 finally 关闭;防重复用标志位或 AbortController |
| 并行选型 | 都要成功用 all;部分失败也要结果用 allSettled |
| React | 在 useEffect 里做请求时,用取消标志防止 setState 到已卸载组件 |
一句话:
异步写多了会自然形成一套习惯,但串行/并行、错误边界、loading 开关这几个点,稍微规范一下就能少踩很多坑。建议把常用的
request和错误/loading 逻辑封装好,业务代码只关心「调接口、处理结果」。
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~