请求乱序——前端开发最容易忽略的bug之一,你中招了吗?

156 阅读5分钟

先抛出一个场景

去年在网易实习的时候,接到过如下图所示的功能需求(并非原UI图,自己手搓的功能一致的UI图),在看到这个需求的一瞬间我就想到了请求乱序的场景

image.png

什么是请求乱序

在计算机领域, 请求乱序(Out-of-Order Requests) 指多个请求(如网络请求、任务处理、数据操作等)的 响应顺序 与它们的 发起顺序不一致

举个国内top大厂淘宝闪购的例子,给大家演示一下请求乱序导致的效果

正常现象:点击奶茶咖啡下方列表正确渲染奶茶咖啡的数据,然后点击超市便利也可以正确渲染数据

20260118202657\_rec\_.gif

请求乱序:操作流程超市便利——>汉堡披萨——>超市便利——>奶茶咖啡快速点击,造成结果:第二次超市便利“正确”的渲染了超市便利的数据,奶茶咖啡渲染了超市便利的数据

20260118205445_rec_.gif

可以看到强如国内top大厂程序员一不小心也会中招这个bug,包括我平时使用的所有这种场景,小至校园墙大至top大厂应用90%都存在这个bug

请求乱序的详细原理

原理如图所示,归根结底还是因为响应时间不一致导致请求与响应的预期顺序不一致,从而导致前端渲染错误

image.png

知道了原理,让我们再分析一下淘宝闪购为什么会出现这种情况
  1. 点击超市便利,超市便利1开始请求
  2. 点击汉堡披萨,汉堡披萨开始请求
  3. 点击超市便利,超市便利2开始请求,超市便利1响应并渲染
  4. 点击奶茶咖啡,超市便利2响应并渲染

根据流程我们可以得知,第二次点击的超市便利看似请求正确了,其实也是请求失败的,而汉堡披萨可能是在超市便利1响应之前或者点击过程中响应了,因为操作过快并未体现在页面上

如何解决请求乱序

当时一共想到了三种解决方案,如下。当然大家如果觉得有什么问题或者有其他方案欢迎讨论

原生的请求中断AbortController

每次请求之前终止上一次的请求

let controller = null;
function getData(currentTab) {
  if (controller) controller.abort();
  controller = new AbortController();
  const signal = controller.signal;
  const res = await fetch("example.com", { signal, id: currentTab })
  setState(res.data)
}

设置对比值

在设置状态的之前进行校验,如果当前响应的是最后一个请求的,才会改变状态 优点是易维护、容易理解;缺点是之前的请求还会继续进行,会浪费服务器资源,不过一般情况下请求数据不大的情况下可以采用这种方式,毕竟用户体验没有影响到,浪费的服务器资源也是公司的

f03db046d3d175219ff8bf1bf4ae7c86.jpg

let globalTab = 'tab1';
async function getData(currentTab) {
  globalTab = currentTab;
  const res = await fetch("example.com", { id: currentTab })
  if(currentTab !== globalTab) return;
  setState(res.data);
}

设置loading和遮罩层状态

默认loading组件自带遮罩层了,就没写了,另外代码只提供思路,实际没有这么简洁,比如需要try catch包起来,false状态设置要在finally里面。这种loading状态是从用户侧直接杜绝用户频繁点击造成请求乱序问题

const [loading,setLoading] = setState(false);
async function getData(currentTab) {
  setLoading(true);
  const res = await fetch("example.com", { id: currentTab })
  setState(res.data);
  setLoading(false);
}

总结与思考

在前端学习的过程中,并非所有技术都需要先动手实操,但一定要对其核心场景和解决思路有基本认知。我此前实习处理请求乱序相关需求时,虽然从未实际遇到过这类场景,但早已了解请求乱序的触发条件 —— 比如先发的请求因服务端处理耗时更长,导致后发的请求反而先响应,也提前梳理过串行请求、标记序号排序、取消旧请求等解决方案。正因为提前对这个场景有认知,才能在需求落地时第一时间预判风险,从源头规避了 bug 的出现。

这种学习思路同样适用于其他技术领域:技术的迭代速度快、内容繁杂,想要面面俱到地掌握所有细节几乎不可能。从学习广度而言,我们更需要先了解各类技术的核心应用场景解决的核心问题—— 就像实习时导师提醒我的,pnpm 的命令繁多,没必要死记硬背,但关键是遇到包管理相关问题时,要知道 pnpm 有对应的命令可以解决;如果连 “有这个技术 / 命令” 都不知道,遇到问题时只会手足无措、毫无头绪。

所以前端学习更需要 “先广后深” 的心态:先建立技术的 “场景认知地图”,知道不同技术能解决什么问题、适用于什么场景,这样在实际开发中遇到对应问题时,才能快速定位到可用的技术方向,而不是像无头苍蝇一样乱撞;至于具体的实操细节和语法,完全可以在需要时再针对性深挖。这种 “先知其然,再知其所以然” 的方式,既能避免无效的死记硬背,也能让我们在面对陌生需求时,始终保持清晰的解决思路。