广告
返回顶部
首页 > 资讯 > 前端开发 > JavaScript >JS前端常见的竞态问题解决方法详解
  • 961
分享到

JS前端常见的竞态问题解决方法详解

JS前端竞态前端竞态解决 2022-11-13 14:11:34 961人浏览 八月长安
摘要

目录什么是竞态问题取消过期请求XMLHttpRequest 取消请求fetch api 取消请求axiOS 取消请求可取消的 promise忽略过期请求封装指令式 promise使用

什么是竞态问题

竞态问题,又叫竞态条件(race condition),它旨在描述一个系统或者进程的输出依赖于不受控制的事件出现顺序或者出现时机。

此词源自于两个信号试着彼此竞争,来影响谁先输出。

简单来说,竞态问题出现的原因是无法保证异步操作的完成会按照他们开始时同样的顺序。举个?:

  • 有一个分页列表,快速地切换第二页,第三页;
  • 先后请求 data2 与 data3,分页器显示当前在第三页,并且进入 loading;
  • 但由于网络的不确定性,先发出的请求不一定先响应,所以有可能 data3 比 data2 先返回;
  • 在 data2 最终返回后,分页器指示当前在第三页,但展示的是第二页的数据。

这就是竞态条件,在前端开发中,常见于搜索,分页,选项卡等切换的场景。

那么如何解决竞态问题呢?在以上这些场景中,我们很容易想到:

当发出新的请求时,取消掉上次请求即可。

取消过期请求

XMLHttpRequest 取消请求

XMLHttpRequest(XHR)是一个内建的浏览器对象,它允许使用 javascript 发送 HTTP 请求。

如果请求已被发出,可以使用 abort() 方法立刻中止请求。

const xhr= new XMLHttpRequest();
xhr.open('GET', 'https://xxx');
xhr.send();
xhr.abort(); // 取消请求

fetch API 取消请求

fetch 号称是 ajax 的替代品,出现于 es6,它也可以发出类似 XMLHttpRequest 的网络请求。

主要的区别在于 fetch 使用了 promise,要中止 fetch 发出的请求,需要使用 AbortController

const controller = new AbortController();
const signal = controller.signal;
fetch('/xxx', {
  signal,
}).then(function(response) {
  //...
});
controller.abort(); // 取消请求

相比原生 API,大多项目都会选择 axios 进行请求。

axios 取消请求

axios 是一个 HTTP 请求库,本质是对原生 XMLHttpRequest 的封装后基于 promise 的实现版本,因此 axios 请求也可以被取消。

可以利用 axios 的 CancelToken API 取消请求。

const source = axios.CancelToken.source();
axios.get('/xxx', {
  cancelToken: source.token
}).then(function (response) {
  // ...
});
source.cancel() // 取消请求

在 cancel 时,axios 会在内部调用 promise.reject() 与 xhr.abort()。

所以我们在处理请求错误时,需要判断 error 是否是 cancel 导致的,避免与常规错误一起处理。

axios.get('/xxx', {
  cancelToken: source.token
}).catch(function(err) { 
  if (axios.isCancel(err)) {
    console.log('Request canceled', err.message);
  } else {
    // 处理错误
  }
});

但 cancelToken 从 v0.22.0 开始已被 axios 弃用。原因是基于实现该 API 的提案 cancelable promises proposal 已被撤销。

v0.22.0 开始,axios 支持以 fetch API 方式的 AbortController 取消请求

const controller = new AbortController();
axios.get('/xxx', {
  signal: controller.signal
}).then(function(response) {
   //...
});
controller.abort() // 取消请求

同样,在处理请求错误时,也需要判断 error 是否来自 cancel。

可取消的 promise

原生 promise 并不支持 cancel,但 cancel 对于异步操作来说又是个很常见的需求。所以社区很多仓库都自己实现了 promise 的 cancel 能力。

我们以awesome-imperative-promise 为例,来看看 cancel 的实现,它的 cancel 实现基于指令式 promise, 源码一共只有 40 行。

什么是指令式 promise?

我们普遍使用的 promise,它的 resolve/reject 只能在 new Promise 内部调用,而指令式 promise 支持在 promise 外部手动调用 resolve/reject 等指令。

通过它的用法能更好地理解何为指令式 promise:

import { createImperativePromise } from 'awesome-imperative-promise';
const { resolve, reject, cancel } = createImperativePromise(promise);
resolve("some value");
// or
reject(new Error());
// or
cancel();

内部的 cancel 方法其实就是将 resolve,reject 设为 null,让 promise 永远不会 resolve/reject。

一直没有 resolve 也没有 reject 的 Promise 会造成内存泄露吗?

有兴趣的同学可以了解下 https://www.jb51.net/article/258149.htm

我个人认为,如果没有保留对 promise 的引用,就不会造成内存泄露。

回到 promise cancel,可以看到,虽然 API 命名为 cancel,但实际上没有任何 cancel 的动作,promise 的状态还是会正常流转,只是回调不再执行,被“忽略”了,所以看起来像被 cancel 了。

因此解决竞态问题的方法,除了「取消请求」,还可以「忽略请求」。

当请求响应时,只要判断返回的数据是否需要,如果不是则忽略即可。

忽略过期请求

我们又有哪些方式来忽略过期的请求呢?

封装指令式 promise

利用指令式 promise,我们可以手动调用 cancel API 来忽略上次请求。

但是如果每次都需要手动调用,会导致项目中相同的模板代码过多,偶尔也可能忘记 cancel。

我们可以基于指令式 promise 封装一个自动忽略过期请求的高阶函数 onlyResolvesLast

在每次发送新请求前,cancel 掉上一次的请求,忽略它的回调。

function onlyResolvesLast(fn) {
  // 保存上一个请求的 cancel 方法
  let cancelPrevious = null; 
  const wrappedFn = (...args) => {
    // 当前请求执行前,先 cancel 上一个请求
    cancelPrevious && cancelPrevious();
    // 执行当前请求
    const result = fn.apply(this, args); 
    // 创建指令式的 promise,暴露 cancel 方法并保存
    const { promise, cancel } = createImperativePromise(result);
    cancelPrevious = cancel;
    return promise;
  };
  return wrappedFn;
}

以上就是 GitHub.com/slorber/awe… 的实现。

只需要将 onlyResolvesLast 包装一下请求方法,就能实现自动忽略,减少很多模板代码。

const fn = (duration) => 
  new Promise(r => {    
    setTimeout(r, duration);  
  });
const wrappedFn = onlyResolvesLast(fn);
wrappedFn(500).then(() => console.log(1));
wrappedFn(1000).then(() => console.log(2));
wrappedFn(100).then(() => console.log(3));
// 输出 3

使用唯一 id 标识每次请求

除了指令式 promise,我们还可以给「请求标记 id」的方式来忽略上次请求。

具体思路是:

  • 利用全局变量记录最新一次的请求 id
  • 在发请求前,生成唯一 id 标识该次请求
  • 在请求回调中,判断 id 是否是最新的 id,如果不是,则忽略该请求的回调

伪代码如下:

let fetchId = 0; // 保存最新的请求 id
const getUsers = () => {
  // 发起请求前,生成新的 id 并保存
  const id = fetchId + 1;
  fetchId = id;
  await 请求
  // 判断是最新的请求 id 再处理回调
  if (id === fetchId) {
    // 请求处理
  }
}

上面的使用方法也会在项目中产生很多模板代码,稍做封装后也能实现一套同样用法的 onlyResolvesLast

function onlyResolvesLast(fn) {
  // 利用闭包保存最新的请求 id
  let id = 0;
  const wrappedFn = (...args) => {
    // 发起请求前,生成新的 id 并保存
    const fetchId = id + 1;
    id = fetchId;
    // 执行请求
    const result = fn.apply(this, args);
    return new Promise((resolve, reject) => {
      // result 可能不是 promise,需要包装成 promise
      Promise.resolve(result).then((value) => {
        // 只处理最新一次请求
        if (fetchId === id) { 
          resolve(value);
        }
      }, (error) => {
        // 只处理最新一次请求
        if (fetchId === id) {
          reject(error);
        }
      });
    })
  };
  return wrappedFn;
}

用法也一样,使用 onlyResolvesLast 包装一下请求方法,实现过期请求自动忽略。

而且,这样的实现不依赖指令式 promise,也更轻量。

「取消」和「忽略」的比较

「取消」更实际

如果请求被「取消」了没有到达服务端,那么可以一定程度减轻服务的压力。

但是取消请求也依赖底层的请求 API,比如 XMLHttpRequest 需要用 abort,而 fetch API 和 axios 需要用 AbortController。

「忽略」更通用

而「忽略」的方式,不依赖请求的 API,更加通用,更容易抽象和封装。本质上所有的异步方法都可以使用 onlyResolvesLast 来忽略过期的调用。

一个更实际,一个更通用,两者的使用需要根据具体场景来权衡。

总结

前端常见的搜索,分页,选项卡等切换的场景中。由于网络的不确定性,先发出的请求不一定先响应,这会造成竞态问题。

解决竞态问题,我们可以选择「取消」或「忽略」过期请求。

  • 「取消请求」,XMLHttpRequest 可以使用 abort 方法,fetch API 以及 axios 可以使用 AbortController
  • 「忽略请求」,可以基于指令式 promise 或请求 id 的方式封装高阶函数来减少模板代码

两种方式各有各的好,需要根据实际场景权衡利弊。

其实解决方式不止这些,像 React Query,GraphQL,rxjs 等都有竞态处理,感兴趣的同学可以再继续深入了解。

以上就是JS前端常见的竞态问题解决方法详解的详细内容,更多关于JS前端竞态的资料请关注编程网其它相关文章!

--结束END--

本文标题: JS前端常见的竞态问题解决方法详解

本文链接: https://www.lsjlt.com/news/171850.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • JS前端常见的竞态问题解决方法详解
    目录什么是竞态问题取消过期请求XMLHttpRequest 取消请求fetch API 取消请求axios 取消请求可取消的 promise忽略过期请求封装指令式 promise使用...
    99+
    2022-11-13
    JS前端竞态 前端竞态解决
  • WCF常见异常问题的解决方法
    这篇文章主要讲解了“WCF常见异常问题的解决方法”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“WCF常见异常问题的解决方法”吧!WCF还是比较常用的,于是我研究了一下WCF,在这里拿出来和大...
    99+
    2023-06-17
  • ResponseBodyAdvice常见问题及解决方法
    这篇文章主要讲解了“ResponseBodyAdvice常见问题及解决方法”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“ResponseBodyAdvice常见问题及解决方法”吧!场景通过R...
    99+
    2023-06-20
  • Nacos的常见问题及解决方法
    本篇内容主要讲解“Nacos的常见问题及解决方法”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Nacos的常见问题及解决方法”吧!如何依赖最新的 Nacos 客户端?很多用户都是通过 Sprin...
    99+
    2023-06-02
  • C++多态性的实现及常见问题解决方法
    C++多态性的实现及常见问题解决方法引言:在C++编程中,多态性是一种重要的概念和特性。它允许我们使用基类的指针或引用来操作派生类的对象,从而实现了程序的灵活性和复用性。本文将介绍C++中多态性的实现方式,并探讨一些常见的多态性问题及其解决...
    99+
    2023-10-22
    多态性 关键词:C++ 问题解决方法 实现:C++多态性问题
  • web开发w如何解决前端常见跨域问题
    这篇文章主要为大家展示了“web开发w如何解决前端常见跨域问题”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“web开发w如何解决前端常见跨域问题”这篇文章吧。什...
    99+
    2022-10-19
  • c#使用listbox的详细方法和常见问题解决
    关于ListBox ListBox是WinForm中的列表控件,它提供了一个项目列表(一组数据项),用户可以选择一个或者多个条目,当列表项目过多时,ListBox会自动添加滚动条,使...
    99+
    2022-11-12
  • Redis常见的几个问题及解决方法
    本篇内容主要讲解“Redis常见的几个问题及解决方法”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Redis常见的几个问题及解决方法”吧! 1、如何保证Redis高可用和高并发Redi...
    99+
    2023-06-16
  • C++中常见的异常处理问题解决方法
    C++中常见的异常处理问题解决方法,需要具体代码示例引言:在编写C++程序时,时常会遇到程序出现异常的情况,如除数为0、数组越界、空指针访问等等。这些异常会导致程序的崩溃或者产生不可预测的结果,为了增强程序的稳定性和可靠性,我们需要使用异常...
    99+
    2023-10-22
    解决方法 异常处理 C++异常
  • MySQL MHA配置的常见问题及解决方法
    这篇文章主要讲解了“MySQL MHA配置的常见问题及解决方法”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“MySQL MHA配置的常见问题及解决方法”吧!...
    99+
    2022-10-18
  • Linux系统常见疑难问题的解决方法
    这篇文章主要介绍“Linux系统常见疑难问题的解决方法”,在日常操作中,相信很多人在Linux系统常见疑难问题的解决方法问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Linux系统常见疑难问题的解决方法”的疑...
    99+
    2023-06-10
  • @ConfigurationProperties使用时常见问题及解决方法
    这篇文章主要介绍“@ConfigurationProperties使用时常见问题及解决方法”,在日常操作中,相信很多人在@ConfigurationProperties使用时常见问题及解决方法问题上存在疑惑,小编查阅了各式资料,整理出简单好...
    99+
    2023-06-20
  • MySQL SSL 连接常见问题及解决方法
    MySQL SSL 连接常见问题及解决方法概述:Secure Socket Layer(SSL)是一种加密传输协议,用于保护数据在网络上的传输安全。MySQL 支持通过 SSL 连接数据库服务器,以增强数据的保密性和完整性。然而,在使用 M...
    99+
    2023-10-22
  • Python中异常处理的常见问题及解决方法
    Python中异常处理的常见问题及解决方法引言:在编写程序时,很难避免出现各种各样的错误和异常。异常处理是一种机制,可以在程序运行时捕获和处理这些异常,从而保证程序的稳定性和可靠性。在Python中,异常处理是一项非常重要的技能,本文将介绍...
    99+
    2023-10-22
    异常处理 (Exception Handling) 解决方法 (Solutions) 常见问题 (Common Prob
  • 使用stream的Collectors.toMap()方法常见的问题及解决
    目录使用stream的Collectors.toMap()方法常见问题在使用过程中有两个小坑需要注意解决方案Stream ToMap(Collectors.toMap) 实践Requ...
    99+
    2023-03-06
    stream Collectors.toMap() Collectors.toMap()方法 使用stream Collectors.toMap()方法
  • web前端中常见的兼容性问题有哪些及怎么解决
    这篇“web前端中常见的兼容性问题有哪些及怎么解决”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“web前端中常见的兼容性问题...
    99+
    2023-06-27
  • JS前端的内存处理的方法全面详解
    目录一、内存的储存和代码执行的场所关系1. 储存空间2. 内存的生命周期3. js 中的内存分配和使用4. 调用栈下移ESP(记录当前执行状态的指针)二、 js中的垃圾回收机制1. ...
    99+
    2023-05-15
    JS前端内存处理 前端内存处理
  • 常见CentOS配置失败的问题及解决方法
    这篇文章主要讲解了“常见CentOS配置失败的问题及解决方法”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“常见CentOS配置失败的问题及解决方法”吧!当CentOS配置失败时,也许你对失败...
    99+
    2023-06-16
  • JavaScript编程中的常见算法问题解决方案
    JavaScript是一种广泛使用的编程语言,其应用范围涉及到Web开发、移动应用开发、游戏开发等领域。在JavaScript编程过程中,常常需要使用到各种算法,例如排序、搜索、字符串处理等。本文将介绍一些JavaScript编程中常见的算...
    99+
    2023-10-15
    编程算法 javascript git
  • CSS使用中常见的问题以及解决方法
    这篇文章主要介绍“CSS使用中常见的问题以及解决方法”,在日常操作中,相信很多人在CSS使用中常见的问题以及解决方法问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”CSS使用中...
    99+
    2022-10-19
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作