解决异步请求竞态问题

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

先说一下问题:

  • 用户输入了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 可以保证请求结果是最新一次请求的结果。

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

相关推荐
光影少年17 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_18 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
susu108301891120 分钟前
vue3 css的样式如果background没有,如何覆盖有background的样式
前端·css
Ocean☾22 分钟前
前端基础-html-注册界面
前端·算法·html
Rattenking22 分钟前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
Dragon Wu24 分钟前
前端 Canvas 绘画 总结
前端
CodeToGym29 分钟前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫30 分钟前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js
~甲壳虫34 分钟前
说说webpack proxy工作原理?为什么能解决跨域
前端·webpack·node.js
Cwhat35 分钟前
前端性能优化2
前端