最近开发需要自己实现一个带搜索的下拉输入框,有搜索功能那必然会存在异步请求时序问题。
先说一下问题:
- 用户输入了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
返回的函数被称为"清除函数",常用来下面两种情况:
- 组件卸载(Unmount) :当组件从DOM中移除,也就是组件即将卸载时,React会自动调用
useEffect
的返回函数。这是最常见的执行时机,用于执行清理工作,比如取消网络请求、移除事件监听器等,以避免内存泄漏或不必要的副作用。 - 依赖项变化前后(Update) :如果你的
useEffect
依赖于某些变量(即在依赖数组中列出的值),并且这些值发生变化导致useEffect
再次执行时,在新的副作用执行前,上一次的清除函数会被调用。这意味着每次依赖项改变导致useEffect
重新运行之前,都会先清理上一次的副作用,确保副作用之间互不影响。
利用useEffect
的"清除函数"和 ignore
可以保证请求结果是最新一次请求的结果。
如果有其他方法,欢迎评论讨论。