前端开发中也会遇到并发问题

1. 前言

在前端开发中,咱们经常得同时向服务器发好几个请求,特别是要获取多个数据资源或者执行多个任务的时候。这篇文章呢,主要是想跟大家分享一些我在实际项目中碰到的前端并发请求问题,还有我是怎么解决这些问题的。希望这些经验能对大家有所帮助哈!

2. 并发请求的场景

咱们聊聊前端并发请求的场景吧,不限于以下几种:

  1. 页面初始化时加载多个数据资源:想象一下,你打开一个网页,它得从服务器那里拿好多东西,比如用户信息、列表数据、配置信息等等。这些请求可能是一起发出去的,这就是页面初始化时的并发请求。
  2. 依赖数据的级联请求:有时候一个请求的结果能告诉咱们下一步该做什么。如果这个链条很长,那同时进行的请求可就多了。
  3. 批量操作:用户执行批量操作(如批量下载、批量更新)时,前端可能会并发地发出多个请求以提高效率。
  4. 实时数据更新:在需要实时更新数据的应用中(如股票交易平台),可能会用到轮询(polling)或WebSocket等技术,这也可能导致并发请求。
  5. 第三方服务的集成:在集成多个第三方服务(如社交媒体分享、地图服务等)时,可能需要并发地请求这些服务的API。

总的来说,前端并发请求的场景真是五花八门,但都是为了给咱们提供更好的用户体验。

3. 遇到的问题

并发请求确实能让应用更快更高效地响应,但也会带来一些挑战:

  1. 性能问题:如果请求太多,服务器可能会感到压力山大,导致响应变慢,甚至影响稳定性。
  2. 资源竞争:浏览器同时发出的请求数量是有限的,超出限制的话,请求就得排队等待,这样可能会让一些紧急的请求也变慢。
  3. 前端资源利用 :并发处理大量请求可能会占用大量的前端资源(如内存和CPU),导致页面响应变慢,影响用户体验。
  4. 数据一致性和同步问题:如果多个请求同时修改同一个数据源,可能会出现数据不一致的情况,需要仔细设计数据同步和状态管理逻辑。
  5. 错误处理复杂化:并发请求的错误处理也更复杂,得考虑怎么集中处理错误,还有部分请求成功、部分失败的情况。

所以,虽然并发请求有很多好处,但也要好好考虑这些挑战,才能确保应用稳定、高效地运行。

4. 解决方案

咱们来聊聊怎么解决这些问题吧!其实有几个小技巧可以试试看。

  • 限制并发数量 :使用队列或Promise.allSettled等方式限制同时进行的请求数量。
  • 使用缓存:我们可以把请求结果缓存起来,这样就不用每次都去麻烦服务器啦。
  • 服务端优化:服务器端也可以优化一下,比如用负载均衡、缓存等方式来提高处理并发请求的能力。
  • 合并请求:如果可能的话,我们还可以把多个请求合并成一个,这样并发数量就减少了。
  • 优先级控制:我们还可以给不同的请求设置优先级,确保重要的请求能先得到处理。

把这些小技巧都用上,我们的应用就能更流畅,用户体验也会变得更好!

5. 案例

5.1 Tab 快速切换竞态条件导致的访问顺序问题

功能描述: 在异常运单列表页面,你会看到4个选项卡,分别是"全部"、"一级"、"二级"和"三级"。每当你切换选项卡时,系统都会根据你的选择重新发送请求,并在收到响应后更新页面上的数据。

问题复现: 想象一下,你像个探险家,一级一级地闯过迷宫,每次点击都像是在探索新的领域。你按顺序发起了三个请求:RequestA、RequestB、RequestC,但网络这个小调皮却把你的响应顺序给搞乱了,变成了Response A、Response C、ResponseB。这可怎么办?页面上的显示和真实的数据就不匹配了,就像你探险时,地图和实际路线不一致一样,真是让人头疼啊!

这种场景还挺多的,比如用户在下单页面上查看费用信息,他们和页面上的表单互动时,那些费用信息就会实时变动,还会调用一些接口。挺酷的吧!

解决方案 :想要解决实时数据频繁更新导致的问题?别担心,我有个小妙招!你可以用一个计数标志来搞定。这样,问题就迎刃而解啦!

js 复制代码
// 使用一个计数变量,每次请求前后进行记录,只有数量一致,才读取数据,避免先发的请求后到达,导致页面查询条件与发起的请求不一致  
  
export function createSequencedRequest(requestFunc) {  
  let requestCount = 0; // 记录当前发起了多少次请求  
  
  const sequencedRequest = (...args) => {  
    const currentRequestCount = ++requestCount; // 记录某个请求的排序  
    return requestFunc(...args).then(r => {  
      if (currentRequestCount === requestCount) {  
        return r;  
      }  
      // 如果请求的顺序不正确,返回一个特殊的值或者错误  
      throw new Error('Request order is incorrect');  
    });  
  };  
  
  return sequencedRequest;  
}

5.2 批量下单并发过多失败问题

功能描述: 用户上传了一个Excel表格,里面填满了订单信息。服务端会解析这些数据,然后把结果反馈给前端展示给用户看。接下来,用户需要为每一个订单补充一些材料。最后,前端会批量调用下单接口,完成所有订单的下单操作。

问题描述: 你知道吗,当普通用户一下子提交几十个单时,他们的浏览器会同时发出几十个请求。这有点像是交通堵塞,请求太多,路就堵了,后面的请求就得等更久才能得到回应。为了避免这种情况,我们在项目中使用了Axios来处理请求,并设置了超时时间,这样请求就不会一直等下去,也不会占用太多资源。

在Chrome浏览器里,如果一下子发出超过6个请求,后面的请求就会因为超时而被取消。所以,为了避免这种情况,我们可以试着分批次下载,或者优化一下我们的请求策略。

由于上线时间问题,这里其实后端提供一个批量下单的接口更加合理。但类似这种情况有大文件分片上传,也会同时发起多个分片请求。

解决方案: 通过控制并发的数量解决问题,每次发起6个请求,只要有一个请求成功响应后,就可以新增一个请求,通过请求队列进行控制。

下面这个函数,就是用来控制流量

js 复制代码
export const pLimit = concurrency => {  
  if (!((Number.isInteger(concurrency) || concurrency === Infinity) && concurrency > 0)) {  
    throw new TypeError('`concurrency` 必须是大于零的整数或者 Infinity');  
  }  
  
  const queue = []; // 用于存储待执行的任务  
  let activeCount = 0; // 表示当前正在执行的任务数量  
  
  /*  
   * 函数用于处理任务执行完成后的逻辑,将 activeCount 减一,并从任务队列中取出下一个任务执行  
   */  
  const next = () => {  
    activeCount--;  
  
    if (queue.length > 0) {  
      queue.shift()(); // 出队  
    }  
  };  
  
  /**  
   * 执行异步任务,并在任务完成后执行 resolve,同时调用 next 函数  
   */  
  const run = async (fn, resolve, ...args) => {  
    activeCount++;  
  
    const result = (async () => fn(...args))();  
  
    resolve(result);  
  
    try {  
      await result;  
    } catch {}  
  
    next();  
  };  
  /**  
   * enqueue 函数用于将任务添加到队列中,并在当前任务数量未达到并发数上限时立即执行   
   */  
  const enqueue = (fn, resolve, ...args) => {  
    // 入队  
    queue.push(run.bind(null, fn, resolve, ...args));  
  
    (async () => {  
      await Promise.resolve();  
      // 立即执行  
      if (activeCount < concurrency && queue.length > 0) {  
        queue.shift()();  
      }  
    })();  
  };  
  
  /**  
   * generator 函数是一个 Promise 化的包装函数,用于生成异步任务的 Promise 对象,并将任务添加到队列中  
   */  
  const generator = (fn, ...args) =>  
    new Promise(resolve => {  
      enqueue(fn, resolve, ...args);  
    });  
  
  // 通过 Object.defineProperties 方法定义了 generator 函数的几个属性  
  Object.defineProperties(generator, {  
    activeCount: {  
      get: () => activeCount,  
    },  
    pendingCount: {  
      get: () => queue.length,  
    },  
    clearQueue: {  
      value: () => {  
        queue.length = 0;  
      },  
    },  
  });  
  
  return generator;  
};

在 pLimit 函数中,generator 函数的设计是为了将异步任务包装成一个 Promise 对象,并将其添加到任务队列中。为了正确处理异步任务的完成状态,需要在异步任务执行完成后调用其对应的 resolve 函数。

在 run 函数中,我们执行了异步任务 fn,然后调用了 resolve 函数,这样可以在异步任务完成后,将结果传递给对应的 Promise 对象。但是,由于 resolve 函数是外部传入的,我们不能直接在 run 函数中调用它,因为在异步任务执行完成后,run 函数已经执行完毕并且无法再直接访问到 resolve 函数。

为了解决这个问题,我们通过闭包的方式,将 resolve 函数传递给 run 函数,并将其保存在 enqueue 函数中调用 run 函数时。这样就能确保在异步任务执行完成后,调用正确的 resolve 函数,将结果传递给对应的 Promise 对象。

使用例子如下:

js 复制代码
// 引入 pLimit 函数  
const { pLimit } = require('./pLimit');  
  
// 创建一个限流器,限制同时执行的任务数量为 2  
const limit = pLimit(2);  
  
// 模拟一个异步任务,返回一个 Promise,其中的延时代表任务执行的耗时  
const asyncTask = async (index) => {  
    console.log(`Task ${index} started`);  
    await new Promise(resolve => setTimeout(resolve, 1000));  
    console.log(`Task ${index} finished`);  
};  
  
// 执行一组任务  
const runTasks = async () => {  
    const tasks = [];  
    for (let i = 1; i <= 5; i++) {  
        tasks.push(limit(() => asyncTask(i)));  
    }  
    await Promise.all(tasks);  
};  
  
// 执行测试  
runTasks();

上面的解决方案抽象起来就是限流限流的核心原理是控制系统在单位时间内能够处理的请求或任务数量,以保护系统不被过载。在实际应用中,限流通常应用于网络通信、接口调用、任务调度等场景。

限流要考虑以下几点:

  1. 并发控制:其实,限流就是为了让系统不要处理太多的请求或任务,避免它累垮了。通过限制同时进行的数量,咱们可以让系统轻松一些,这样它就能更稳定、更可靠地工作了。
  2. 算法选择: 选择合适的限流算法是限流实现的关键。常见的限流算法包括令牌桶算法、漏桶算法、计数器算法等。这些算法在实现上各有特点,例如令牌桶算法可以平滑限制请求的流量,漏桶算法可以平滑处理突发流量等。
  3. 计数器和队列: 限流通常使用计数器和队列来跟踪和控制请求或任务的数量。计数器用于统计当前活跃的请求数量或任务数量,而队列则用于暂存超出限流阈值的请求或任务,以便后续处理。
  4. 动态调整: 限流策略通常需要根据系统负载和性能动态调整。例如,当系统负载较低时可以适当放宽限流策略,以提高系统的响应速度;当系统负载较高时则需要加强限流策略,以保护系统不被过载。
  5. 错误处理: 在限流过程中,需要考虑如何处理请求被拒绝的情况。合适的错误处理策略可以提高系统的用户体验和容错能力,例如返回友好的错误提示、重试机制等。

总的来说,限流的核心原理是通过控制系统的并发数量、选择合适的限流算法、使用计数器和队列进行跟踪和控制、动态调整限流策略以及合理处理被拒绝请求等方式,保护系统不被过载,确保系统的稳定性和可靠性。

借鉴后端的限流原理,我们来实现在前端控制并发请求数。

总结

不只是后端开发者要头疼并发问题,有时候前端也需要掌握一些并发的解决方案。比如说,在需要批量操作或者实时数据更新的场景下,前端也会遇到性能、数据一致性等头疼的问题。不过别担心,我们可以通过限流、优先级等方案来解决这些问题,可以考虑一些开源方案比如 p-limit、react query、swr 等。

相关推荐
fishmemory7sec3 分钟前
Electron 主进程与渲染进程、预加载preload.js
前端·javascript·electron
fishmemory7sec6 分钟前
Electron 使⽤ electron-builder 打包应用
前端·javascript·electron
豆豆1 小时前
为什么用PageAdmin CMS建设网站?
服务器·开发语言·前端·php·软件构建
JUNAI_Strive_ving1 小时前
番茄小说逆向爬取
javascript·python
看到请催我学习1 小时前
如何实现两个标签页之间的通信
javascript·css·typescript·node.js·html5
twins35202 小时前
解决Vue应用中遇到路由刷新后出现 404 错误
前端·javascript·vue.js
qiyi.sky2 小时前
JavaWeb——Vue组件库Element(3/6):常见组件:Dialog对话框、Form表单(介绍、使用、实际效果)
前端·javascript·vue.js
煸橙干儿~~2 小时前
分析JS Crash(进程崩溃)
java·前端·javascript
哪 吒2 小时前
华为OD机试 - 几何平均值最大子数(Python/JS/C/C++ 2024 E卷 200分)
javascript·python·华为od
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_027】3.4 让 D3 数据适应屏幕(下)—— D3 分段比例尺的用法
前端·javascript·信息可视化·数据可视化·d3.js·d3比例尺·分段比例尺