JavaScript 多并发请求处理方案详解

晚来风
晚来风
发布时间:2026-10-13 | 6人浏览过
原创

在网页开发中,当用户进入一个包含复杂数据的页面时,通常需要同时发起多个异步网络请求(如查询常用地址、商品信息、城市列表等)。这些请求并行执行以提高加载速度,但业务逻辑往往要求所有数据都获取成功后才能渲染页面或允许用户操作。本文将深入剖析如何优雅地解决这一“等待所有并发结束”的场景。

核心挑战:如何等待多个异步请求完成

前端页面的性能很大程度上依赖于并发请求的能力。如果为了等待数据而阻塞主线程,会导致页面出现卡顿(White Screen);如果无法准确判断所有请求何时结束,则容易导致页面元素未就绪时的错误操作。我们需要一种机制来监听多个独立异步任务的状态,并在它们全部完成后触发特定的逻辑。

传统解决方案及其局限

在ES6标准普及之前,开发者们摸索出了多种应对并发计数的方法,虽然部分思路已逐渐淘汰,但理解其原理有助于掌握异步执行的底层逻辑。

第1步:理解早期常见的并发处理方式

以下是几种历史上被广泛提及的思路:

  • 改为串行请求:将一个请求的完成作为下一个请求的触发条件。虽然能解决顺序依赖问题,但由于每个请求都需要等待前一个响应,总耗时是所有请求耗时之和,会严重拖慢页面加载速度。
  • 回调函数嵌套:在第一个请求的回调中发起第二个请求。对于两个并发请求来说尚可接受,但随着并发数量增加,代码缩进会越来越深,形成所谓的“回调地狱”,导致代码可读性极差且难以维护。
  • 将异步请求设为同步(Sync):例如在 jQuery 中将 async 参数设置为 false。这种做法虽然简单粗暴地实现了顺序执行,但它会完全冻结浏览器界面,导致用户在等待期间无法进行任何交互,体验极其糟糕,属于应当避免的反模式。
// 设置同步请求(不推荐!)
$.ajax({
  url:"/api/data",
  async:false
});

第2步:通过计数器追踪并发进度

既然必须保持并发以提升性能,那么“计数法”便应运而生。其核心逻辑是预先知道需要多少个接口,每完成一个就递增计数器,当计数器达到预设值时,视为全部成功。

var total = 2;
var completedCount = 0;

function callback() {
  completedCount++;
  if (completedCount === total) {
    console.log('所有并发请求已执行完毕');
  }
}

为了实现这个计数过程,通常配合以下几种技术手段:

  1. 简单的闭包与函数内调用:在每个异步函数的内部手动调用 callback()。
  2. 循环阻塞(死循环检查):使用一个 `while` 循环不断检查变量。这会导致 JS 引擎的主线程卡死,同样会造成页面假死,绝对不可取。
  3. 定时器非阻塞轮询:使用 setInterval 定时检查计数器的状态。这种方法不会阻塞线程,但高频轮询会浪费系统资源,且在页面关闭时需要手动清除定时器,增加了复杂度。
var total = 2;
var completedCount = 0;
var interval = setInterval(function(){
  if(completedCount === total){
    clearInterval(interval);
    console.log('已完成');
  }
}, 50); // 间隔50毫秒检查一次,效率较低

现代 JavaScript 并发最佳实践

随着 ECMAScript 标准的演进,现在有更多高级原语可以优雅地处理这种多并发场景。

第3步:利用 Promise.all 实现并发汇总

Promise.all 是处理 Promise 并发最标准的方法。它将多个 Promise 实例包装成一个新的 Promise,只有当所有传入的 Promise 都成功(resolve)时,新 Promise 才会成功,并将结果数组按原顺序返回;只要有一个失败(reject),整个 Promise 就会立即失败。

// 假设 fetchAddress, fetchProducts 是返回 Promise 的异步函数
Promise.all([
  fetchAddress(),
  fetchProducts()
])
.then(function(results) {
    var addressData = results[0];
    var productsData = results[1];
    renderPage(addressData, productsData);
})
.catch(function(error) {
    // 任何一个请求失败都会在这里捕获
    handleError(error);
});

如果你使用的是较新的项目脚手架,原生 Fetch API 已经提供了更简洁的方案。

第4步:使用 Async/Await 简化同步逻辑

如果需要在获取完所有数据后,再串行执行一些复杂的组装逻辑,结合 Promise.all 和 async/await 能让代码看起来像同步代码一样直观。

async function initializePage() {
  try {
    // 并行发起请求,await 确保所有数据准备就绪
    const [addressInfo, productInfo] = await Promise.all([
      fetchAddress(),
      fetchProducts()
    ]);

    // 此时所有的数据都已可用,安全地更新 UI
    updateUIWith(addressInfo, productInfo);
  } catch (error) {
    showErrorMessage('页面加载失败,请重试');
  }
}

错误处理与边界情况思考

在实际项目中,并发请求很少能保证永远 100% 成功。我们需要考虑到网络超时或服务器报错的情况。

  • 容错处理:如果某一个子请求失败了,是否需要阻断整个页面?如果是强依赖型数据(如订单号不存在),则应停止。如果某个弱依赖数据(如推荐列表)失败不应影响主流程,可以使用 .catch() 单独为每一个 Promise 挂载失败处理函数,或者使用 Promise.allSettled(注意此方法会等待所有完成,无论成败,然后返回对象数组描述每个 Promise 的结果)。
// 即使个别请求失败也不影响最终拿到其他成功的数据
Promise.allSettled([
  fetchAddress(),
  fetchCacheData() // 可能会失败的缓存读取
])
.then(settlements => {
   const successfulResults = settlements.filter(s => s.status === 'fulfilled').map(s => s.value);
   // ... 基于成功的结果继续渲染
});

以上就是对 JavaScript 如何处理多并发问题的详细内容,更多关于前端并发编程与异步请求优化的资料请关注本站其它相关文章!

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表