前端进阶:从请求竞态到并发控制(系统学习笔记)

前言

今天主要围绕前端开发中一个非常核心但又容易被忽视的问题展开:

请求竞态 & 并发控制

这类问题在实际项目(尤其是搜索、列表加载、数据请求)中非常常见,同时也是面试高频考点。

本文会从基础到进阶,系统梳理:

  • 什么是请求竞态
  • 如何解决竞态问题
  • AbortController 的底层原理
  • React 中如何优雅处理请求
  • 什么是并发请求
  • 如何实现并发控制(核心算法)

一、什么是请求竞态(Race Condition)

典型场景:搜索框

用户输入:

复制代码
a → ab → abc

前端发出请求:

复制代码
req1: a
req2: ab
req3: abc

但返回顺序可能是:

复制代码
req3 → req1 → req2

最终 UI 显示的是旧数据(比如 aba)。

本质

请求的返回顺序 ≠ 请求的发送顺序


二、解决请求竞态的三种方案

方案1:请求打标(版本控制)

思路

每个请求分配唯一 ID(或时间戳),只处理"最新请求"的结果。

示例代码
js 复制代码
let currentRequestId = 0;

function search(keyword) {
  const requestId = ++currentRequestId;

  fetch(`/api?q=${keyword}`)
    .then(res => res.json())
    .then(data => {
      if (requestId !== currentRequestId) return;
      render(data);
    });
}
优点
  • 简单稳定
  • 不依赖浏览器能力
缺点
  • 请求仍然发送(浪费资源)

方案2:取消请求(AbortController)

思路

每次新请求前取消旧请求。

js 复制代码
let controller;

function search(keyword) {
  if (controller) controller.abort();

  controller = new AbortController();

  fetch(`/api?q=${keyword}`, {
    signal: controller.signal
  });
}

注意需要过滤取消错误:

js 复制代码
.catch(err => {
  if (err.name === 'AbortError') return;
});
优点
  • 真正取消请求
  • 更节省资源

方案3:防抖(Debounce)

思路

用户停止输入一段时间后才发请求,从源头减少请求数量。


三、AbortController 底层原理

核心机制

  1. fetch 监听 signal
  2. abort() 触发事件
  3. 浏览器网络层中断请求

本质

JS 发出信号 → 浏览器执行中断

不同阶段的行为

阶段 行为
请求未发送 直接取消
请求中 中断连接
响应中 停止读取数据

四、React 中的正确写法

错误示例

jsx 复制代码
useEffect(() => {
  fetch(`/api?q=${keyword}`)
    .then(res => res.json())
    .then(data => setData(data));
}, [keyword]);

问题:

  • 请求竞态
  • 组件卸载后仍 setState(内存泄漏)

正确写法

jsx 复制代码
useEffect(() => {
  const controller = new AbortController();

  fetch(`/api?q=${keyword}`, {
    signal: controller.signal
  })
    .then(res => res.json())
    .then(data => setData(data))
    .catch(err => {
      if (err.name === 'AbortError') return;
      console.error(err);
    });

  return () => {
    controller.abort();
  };
}, [keyword]);

核心点

  • 每次 effect 执行 → 创建 controller
  • cleanup 函数 → 取消上一次请求

五、什么是并发请求

定义

多个请求同时需要执行,而不是互相替代。

示例

js 复制代码
fetch('/user');
fetch('/posts');
fetch('/notifications');

和竞态的区别

类型 特点 是否需要取消
竞态 新请求替代旧请求
并发 多请求同时需要

六、并发问题的核心

并发场景要解决的不是"取消",而是:

  1. 结果管理
  2. 错误隔离
  3. 并发数量控制

七、实现并发控制(重点)

目标

限制同时最多 N 个请求。

核心思想

任务池 + 补位机制

完整实现(推荐掌握)

js 复制代码
function limitRequests(tasks, limit) {
  return new Promise((resolve) => {
    let i = 0;
    let finished = 0;
    const results = [];

    function run() {
      if (i >= tasks.length) return;

      const currentIndex = i;
      const task = tasks[i++];

      task()
        .then((res) => {
          results[currentIndex] = res;
        })
        .catch((err) => {
          results[currentIndex] = err;
        })
        .finally(() => {
          finished++;

          if (finished === tasks.length) {
            resolve(results);
          } else {
            run(); // 补位
          }
        });
    }

    for (let j = 0; j < limit; j++) {
      run();
    }
  });
}

执行流程

  1. 先执行 limit 个任务
  2. 任意任务完成 → 补一个新任务
  3. 循环直到完成

八、整体知识体系总结

三类问题及解法:

问题 解决方案
请求竞态 打标 / AbortController
并发请求 Promise.all / allSettled
并发过多 并发控制(limit)

九、面试总结话术

请求竞态

通过请求打标或 AbortController 控制请求生命周期,避免旧请求覆盖新数据。

AbortController

基于事件机制,通过 signal 通知浏览器网络层中断请求。

并发控制

通过维护执行池,控制最大并发数,并在任务完成后动态补位。


后续学习方向

下一步可以继续深入:

  • 并发控制 + 重试机制
  • 请求超时设计
  • 请求缓存(Cache)
  • React Hook 封装(useRequest)

总结

今天你已经掌握了一条非常重要的前端能力链路:

请求竞态 → 请求取消 → React 实战 → 并发理解 → 并发控制

这套知识在实际开发和面试中都非常有价值。

下一步建议:请求库封装(工程级 useRequest 实现),将本文所有知识整合成一个真实项目级方案。

相关推荐
大、男人2 小时前
edge浏览器打开baidu.com很慢,我是如何解决的
前端·edge
吴声子夜歌2 小时前
ES6——函数的扩展详解
前端·ecmascript·es6
山甫aa2 小时前
STL---常见数据结构总结
开发语言·数据结构·c++·学习
有趣的老凌2 小时前
一篇文章带你了解 Agent Skills —— 告别AI“失控”
前端·agent·claude
~ rainbow~2 小时前
前端转型全栈(二)——NestJS 入门指南:从 Angular 开发者视角理解后端架构
前端·javascript·angular.js
小琪爱学习2 小时前
项目学习代码
学习
恋猫de小郭2 小时前
AGP 9.2 开始,Android 上协程启动和取消速度提升两倍
android·前端·flutter
Ulyanov2 小时前
Python与YAML的优雅交响:从配置管理到数据艺术的完美实践 (一)
开发语言·前端·python·数据可视化