解决异步请求竞态问题

最近开发需要自己实现一个带搜索的下拉输入框,有搜索功能那必然会存在异步请求时序问题。

先说一下问题:

  • 用户输入了a,触发了请求 A,
  • 接着用户又输入了aa,触发了请求 AA
  • 接着用户又输入了aaa,触发了请求 AAA

很有可能,AAA 先返回了数据,很快 A 又返回了数据,AA 接着也返回了数据,如果不做任何处理,那用户很有可能看到的是 AA 返回的请求。这就造成了请求的时序混乱的问题。怎么解决这个问题呢?

模拟请求并简化代码如下:

html 复制代码
  <button onclick="handleFetch()">fetch</button>
  <script>
    const mockFetch = () => fetch('https://dog.ceo/api/breeds/image/random').then((res) => {
      return res.json();
    })
    let globalResult;
    const handleFetch = () => {
      mockFetch().then((data) => {
        globalResult = data;
        console.log(data)
      })
    }
  </script>

这里推荐使用 github 提供的免费API:github.com/public-apis...

Promise 封装

js 复制代码
let cancel = () => {}

const fetchWithCancel = () => {
  cancel('取消请求')
  return new Promise((resolve, reject) => {
    cancel = reject;
    mockFetch().then(res=>{
      resolve(res)
    }).catch(err=>{
      reject(err)
    })
  })
}

const handleFetch = () => {
  fetchWithCancel().then(data => {
    globalResult = data;
    console.log(data);
  }).catch(err=>{
    console.log(err);
  })
}

对请求数据 mockFetch 封装一层 Promise,并把控制 Promise 状态的 reject 函数抛出来。每次触发新的请求时,先结束掉前一个 Promise 的状态,这样就能保证在修改数据的时候只保留了最后一个 Promise,保证异步请求的时序。

AbortController 中止请求

AbortController 可以用来中止 fetch 请求。请看 VCR,哦不是请看代码:

js 复制代码
const mockFetch = (signal) => fetch('https://dog.ceo/api/breeds/image/random', {signal}).then((res) => {
  return res.json();
})

let globalResult;
let controller;
const handleFetch = () => {
  if (controller) {
    controller.abort('取消请求');
  }
  controller = new AbortController();
  mockFetch(controller.signal).then(res=>{
    globalResult = res;
    console.log(res);
  }).catch(err=>{
    console.log(err);
  })
}

封装 fetch 请求的时候需要把 signal 参数传进去, controller 是一个全局变量,可以保留上一次的请求的 signal,每次触发新的请求之前,会先将之前的 fetch 请求中止掉,浏览器会显示 canceled 状态。

这样就可以保证,只会处理最近一次的请求。

类似的,使用 Axios 封装请求的话,也可以传入对应的标志,来先取消之前的请求:

js 复制代码
import axios from 'axios';
 
const mockFetch = (cancelToken) =>  axios.get('/api/data', {
  cancelToken
})

let source;
let globalResult;
const handleFetch = () => {
  if (source) {
    source.cancel('请求取消的原因');
  }
  // 创建取消标记
  source = axios.CancelToken.source();
  mockFetch(source.token).then(res=>{
    globalResult = res;
    console.log(res);
  }).catch(err=>{
    console.log(err);
  })
}

利用 react 的 useEffect

在 react 官网也给出了一种方案:zh-hans.react.dev/reference/r...

tsx 复制代码
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);

  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    };
  }, [person]);

  // ...

我们知道 useEffect 返回的函数被称为"清除函数",常用来下面两种情况:

  1. 组件卸载(Unmount) :当组件从DOM中移除,也就是组件即将卸载时,React会自动调用useEffect的返回函数。这是最常见的执行时机,用于执行清理工作,比如取消网络请求、移除事件监听器等,以避免内存泄漏或不必要的副作用。
  2. 依赖项变化前后(Update) :如果你的useEffect依赖于某些变量(即在依赖数组中列出的值),并且这些值发生变化导致useEffect再次执行时,在新的副作用执行前,上一次的清除函数会被调用。这意味着每次依赖项改变导致useEffect重新运行之前,都会先清理上一次的副作用,确保副作用之间互不影响。

利用useEffect 的"清除函数"和 ignore 可以保证请求结果是最新一次请求的结果。

如果有其他方法,欢迎评论讨论。

相关推荐
一个处女座的程序猿O(∩_∩)O9 分钟前
完成第一个 Vue3.2 项目后,这是我的技术总结
前端·vue.js
mubeibeinv9 分钟前
项目搭建+图片(添加+图片)
java·服务器·前端
逆旅行天涯16 分钟前
【Threejs】从零开始(六)--GUI调试开发3D效果
前端·javascript·3d
m0_7482552637 分钟前
easyExcel导出大数据量EXCEL文件,前端实现进度条或者遮罩层
前端·excel
长风清留扬1 小时前
小程序毕业设计-音乐播放器+源码(可播放)下载即用
javascript·小程序·毕业设计·课程设计·毕设·音乐播放器
web147862107231 小时前
C# .Net Web 路由相关配置
前端·c#·.net
m0_748247801 小时前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
飞的肖1 小时前
前端使用 Element Plus架构vue3.0实现图片拖拉拽,后等比压缩,上传到Spring Boot后端
前端·spring boot·架构
青灯文案11 小时前
前端 HTTP 请求由 Nginx 反向代理和 API 网关到后端服务的流程
前端·nginx·http
m0_748254881 小时前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl