同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
写了那么久前端,接口请求写了那么多次——但每次碰到「先查 A 再根据 A 的结果查 B」还是习惯一层层回调?
Promise.all和Promise.allSettled到底该用哪个?错误该在哪里统一处理?loading 开着关不上、重复请求怎么防?这篇文章不讲事件循环底层,就一个目标:让你看完以后,接口请求能手写、能选对方案、能少踩坑。
一、异步到底在解决什么问题
一句话定义: 异步是为了在「等待耗时操作(如接口请求)」时,不阻塞主线程,让页面继续响应用户操作。
| 场景 | 处理方式 |
|---|---|
| 用户点击按钮发起登录请求 | 等接口返回,再跳转或提示 |
| 页面加载时拉取用户信息 + 列表数据 | 多个接口并行请求,等全部完成再渲染 |
| 先查省市区,再根据选中的省查市 | 串行请求,后者依赖前者 |
| 上传文件时显示进度条 | 监听 xhr 或 fetch 的进度事件 |
为什么不用同步? 因为 JS 单线程,同步等接口会卡住整个页面。所以需要「发起请求 → 去做别的事 → 请求好了再回来处理」。
二、Promise 基础:搞清楚「状态」和「链式」
2.1 三种状态
pending(进行中) → fulfilled(成功) 或 rejected(失败)
| 单词 | 国际音标(美式) | 国际音标(英式) | 中文释义 |
|---|---|---|---|
| pending | /ˈpendɪŋ/ | /ˈpendɪŋ/ | 进行中 |
| fulfilled | /fʊlˈfɪld/ | /fʊlˈfɪld/ | 已成功 |
| rejected | /rɪˈdʒektɪd/ | /rɪˈdʒektɪd/ | 已失败 |
状态一旦变更就不能再改。then 只会执行一次(成功走第一个回调,失败走第二个或 catch)。
2.2 链式调用的本质
fetch('/api/user')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
fetch说明:fetch 是浏览器原生提供的 API(无需引入任何库),基于 Promise 实现(所以支持 async/await 语法),专门用来发送 HTTP 请求(GET/POST/PUT/DELETE 等)、获取远程资源(接口数据、文件、图片等)
.json()说明:
.json() → 前端最常用的 “数据解析器”
- 所有返回 JSON 格式的接口(比如登录、查列表、获取用户信息)都要用它;
- 注意:如果响应内容不是合法 JSON(比如后端返回了纯文本),调用
.json()会报错,需要用 try-catch 捕获;
fetch 响应解析方法对照表:
| 解析方法 | 核心作用 | 适用场景 |
|---|---|---|
.json() | 将响应体解析为 JavaScript 对象/数组 | 接口返回 JSON 格式数据(最常用,如接口返回 {name:"张三"} 或 [1,2,3]) |
.text() | 将响应体解析为 纯文本字符串 | 返回 HTML 片段、纯文本、XML 等(如请求一个 .txt 文件、后端返回的纯文本提示) |
.blob() | 将响应体解析为 二进制 Blob 对象 | 图片、视频、音频、PDF 等二进制文件(如下载图片、预览上传的文件) |
.formData() | 将响应体解析为 FormData 表单对象 | 后端返回表单格式数据(极少用,更多是前端用 FormData 提交数据) |
每一层 then 都返回一个新的 Promise。如果回调里 return 了值,下一个 then 会拿到这个值;如果 return 了 Promise,下一个 then 会等这个 Promise 完成。
常见误区: 以为 catch 只接上面那一层 then 的错误。其实 catch 会捕获前面整条链上未处理的 rejection。
rejection说明:
rejection(名词,发音 /rɪˈdʒekʃn/)是 Promise 状态变为 rejected(已失败)时产生的 “失败信号 / 异常结果”,可以理解为:
Promise 执行失败了,抛出的 “错误信息 / 失败原因” 就是 rejection。
简单类比:
Promise就像一个 “任务包裹”;fulfilled(成功)→ 包裹里是 “正确结果”(对应resolve);rejected(失败)→ 包裹里是 “错误原因”(对应reject),这个 “错误原因” 就是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 会怎样?
try-catch 是 async/await 里 “接住错误” 的网,没这张网,错误就会飘到全局,变成难调试的 “未处理 rejection”~
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 防止重复提交:用标志位
<template>
<button
@click="handleSubmit"
:disabled="isLoading"
>
{{ isLoading ? '提交中...' : '提交' }}
</button>
</template>
<script setup>
// 1. 引入 ref 用于声明响应式状态(替代 React 的 useState)
import { ref } from 'vue'
// 2. 声明加载状态,初始值为 false(对应 React 的 useState(false))
const isLoading = ref(false)
// 3. 模拟提交请求的异步函数
const submitRequest = async () => {
// 模拟接口请求延迟 2 秒
return new Promise((resolve) => {
setTimeout(() => {
resolve('提交成功')
}, 2000)
})
}
// 4. 点击提交的处理函数(核心防重复提交逻辑)
const handleSubmit = async () => {
// 如果正在加载,直接返回,阻止重复点击
if (isLoading.value) return
try {
// 开始提交,设置加载状态为 true(禁用按钮)
isLoading.value = true
// 调用接口提交数据
const result = await submitRequest()
console.log(result)
alert('提交成功!')
} catch (error) {
console.error('提交失败:', error)
alert('提交失败,请重试!')
} finally {
// 无论成功/失败,最终都恢复加载状态为 false(启用按钮)
isLoading.value = false
}
}
</script>
6.4 防重复请求:AbortController
- 英文全称:Abort Controller
- 音标:/əˈbɔːt kənˈtrəʊlə(r)/
- 中文谐音:呃-波特 / 肯-吹-勒(大致发音,可参考词典音频)
- 中文翻译:中止控制器
AbortController 是浏览器提供的一个 Web API,专门用来取消异步操作(比如网络请求、定时器等)。
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;
}
}
在这段代码里,它的作用是:
-
当用户快速连续触发搜索(比如输入框实时联想)时,前一个未完成的请求会被自动取消,只保留最新的那次请求。
-
这样既避免了无效请求浪费服务器资源,也防止了旧请求的返回结果覆盖最新结果。
核心概念拆解
-
创建控制器:
const controller = new AbortController();每次发起新请求时,先创建一个新的
AbortController实例。 -
传递信号:
fetch('/api/search', { signal: controller.signal });把控制器的
signal(信号)传给fetch,让它和这个请求绑定。 -
取消请求:
controller.abort();调用
abort()方法,就能立即终止绑定了这个signal的请求。 -
处理取消错误:
请求被取消后,
fetch会抛出一个名为AbortError的错误,我们需要在catch里捕获并忽略它,避免控制台报错。
七、真实踩坑案例
坑 1:for 循环里 await 写成串行(浪费请求时间)
场景:批量请求接口(如根据多个ID获取数据),本意是让所有请求并行执行,提升效率,却因写法错误导致串行执行,耗时翻倍。
// ❌ 错误写法:本意是并行,实际是串行(一个请求完成才会发起下一个)
const ids = [1, 2, 3];
const results = [];
for (const id of ids) {
const res = await fetch(`/api/item/${id}`); // 每次await都会阻塞循环
results.push(await res.json());
}
// ✅ 正确写法:真正并行(同时发起所有请求,等待全部完成)
const ids = [1, 2, 3];
// 用Promise.all包裹所有请求,批量并行执行
const results = await Promise.all(
ids.map(id => fetch(`/api/item/${id}`).then(r => r.json()))
);
避坑要点:Vue中批量请求时,避免在for循环内直接写await,优先使用Promise.all,减少请求总耗时;若需限制并行数量,可搭配第三方工具(如p-limit)。
坑 2:忘记在 catch 里重新 throw(上层无法感知错误)
场景:异步请求失败后,仅提示错误信息,未重新抛出错误,导致调用该方法的上层代码无法判断请求是否成功,进而出现逻辑异常(如拿到undefined继续执行)。
// ❌ 错误写法:catch中未throw,调用方拿不到失败信息
async function loadData() {
try {
return await fetch('/api/data'); // 模拟Vue项目中接口请求
} catch (err) {
// 仅提示错误,未抛出,上层会认为请求“成功”
ElMessage.error('数据加载失败'); // Vue中常用Element Plus提示
}
}
// 调用方(Vue组件中)
const data = await loadData();
// 失败时data是undefined,若后续用data做操作(如data.list),会报“undefined”错误
// ✅ 正确写法:catch中重新throw,让上层感知错误并处理
async function loadData() {
try {
return await fetch('/api/data');
} catch (err) {
ElMessage.error('数据加载失败');
throw err; // 关键:重新抛出错误,上层可通过try/catch捕获
}
}
// 调用方(Vue组件中)
try {
const data = await loadData();
// 只有请求成功才执行后续逻辑
handleData(data);
} catch (err) {
// 失败时做额外处理(如重置状态)
resetData();
}
避坑要点:Vue中封装异步请求方法时,若需要上层组件感知错误、做后续处理,catch中必须重新throw错误;若无需上层处理,可在catch中直接处理完所有异常(如重置页面状态)。
坑 3:接口返回的「业务错误」没当错误处理(逻辑异常)
场景:接口HTTP状态码为200(请求成功),但返回的业务状态码异常(如code≠0),未主动抛出错误,导致后续逻辑继续执行,出现异常(如渲染错误数据)。
// ❌ 错误写法:仅判断HTTP成功,未处理业务错误
async function submitForm(formData) {
const res = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(formData)
});
const data = await res.json();
// 忽略业务错误(如code=400,参数错误),直接返回数据
return data.data; // 业务错误时data.data可能不存在,导致后续报错
}
// ✅ 正确写法:主动判断业务状态,异常时throw错误
async function submitForm(formData) {
const res = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(formData)
});
const data = await res.json();
// 接口约定:code=0为业务成功,非0为业务错误(如参数错误、权限不足)
if (data.code !== 0) {
// 主动抛出错误,触发catch逻辑
throw new Error(data.message || '提交失败');
}
return data.data; // 只有业务成功,才返回有效数据
}
避坑要点:Vue项目中,接口请求需同时判断「HTTP状态」和「业务状态」;大多数后端接口会约定code字段,务必在请求后校验,业务错误时主动throw,避免后续逻辑异常。
八、实战最佳实践总结
| 维度 | 建议 |
|---|---|
| 串行 vs 并行 | 无依赖用 Promise.all 并行;有依赖就老老实实串行 await |
| 错误处理 | async 函数内必有 try-catch;统一封装可放在 request 层 |
| Loading | 用 finally 关闭;防重复用标志位或 AbortController |
| 并行选型 | 都要成功用 all;部分失败也要结果用 allSettled |
| React | 在 useEffect 里做请求时,用取消标志防止 setState 到已卸载组件 |
一句话:
异步写多了会自然形成一套习惯,但串行/并行、错误边界、loading 开关这几个点,稍微规范一下就能少踩很多坑。建议把常用的
request和错误/loading 逻辑封装好,业务代码只关心「调接口、处理结果」。
🔍 本系列专栏导航
一、《闭包实战:从原理到防抖・节流・缓存|JS 进阶必会篇》
二、《异步基础:Promise & async/await 实战|JS 进阶必会篇》
三、《常用工具函数:深拷贝・去重・扁平化手写实战|JS 进阶必会篇》
四、《事件循环与宏微任务:log 顺序实战解析|JS 进阶必会篇》
五、《设计模式实战:单例・发布订阅・策略 JS 轻量用法|JS 进阶必会篇》
六、《浏览器存储实战:localStorage/sessionStorage/cookie 用法详解|JS 进阶必会篇》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~