异步基础:Promise、async/await 在接口请求中的最佳实践

0 阅读6分钟

同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端正则干货,

咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。

写了那么久前端,接口请求写了那么多次——但每次碰到「先查 A 再根据 A 的结果查 B」还是习惯一层层回调?

Promise.allPromise.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 层
Loadingfinally 关闭;防重复用标志位或 AbortController
并行选型都要成功用 all;部分失败也要结果用 allSettled
React在 useEffect 里做请求时,用取消标志防止 setState 到已卸载组件

一句话:

异步写多了会自然形成一套习惯,但串行/并行、错误边界、loading 开关这几个点,稍微规范一下就能少踩很多坑。建议把常用的 request 和错误/loading 逻辑封装好,业务代码只关心「调接口、处理结果」。


学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。

后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。

关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。

如果你觉得这篇内容对你有帮助,不妨点赞收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。

我是 Eugene,你的电子学友,我们下一篇干货见~