我们查询的是生产条件下的数据,可是查出来的数据的环境类型却是显示的是测试,遇到这个问题,第一反应是后端的问题,经过一系列排查后,发现后端返回的数据是正确的,是前端的渲染有问题,正常的数据显示应该是如下图所示:
为什么会出现以上的情况了?
在以下的讲解中,用task1对应点击测试触发的异步任务,task2对应点击预发布对应的异步任务,task3对应点击发布触发的异步任务。
用户每次点击tab切换都会触发一次网络请求(依次为task1 -> task2 -> task3),我们也期望的是以task1 -> task2 -> task3 的顺序响应给应用。但是因为是异步请求,由于网络状况的不可控和其他因素,接口的响应时间并不确定,也就是说。有可能后面触发的请求先于前面的请求得到了响应。
看如下的响应时间图:
task1需要3s的时间响应,task2需要2s的时间响应,task1需要1s的时间响应,这时就出现了状态的竞争,因为你在task3响应时渲染的结果,很快就被task2的响应覆盖了,又很快被task1的响应覆盖了,此时接口响应的顺序为 task3->task2->task1,而此时你页面上最终需要展示的是task3的响应数据,但是task3的数据最终会被task1的响应的数据覆盖掉,这样就会导致页面数据错乱。
导致页面数据错乱的主要原因是获取数据是异步请求,异步请求的响应时间不确定,后面发出的请求有可能会把前面发出的请求返回快,出现了状态的竞态。
出现这样的问题改如何解决了?
实现思路:不同的筛选条件存储不同的数据,不共用一份数据,设置一个对象存储数据,请求的参数作为key, 请求的结果作为value, 每次请求完后都将数据存入接收对象,使用时通过key去取数据。
代码示例
import React, {useState, useEffect} from 'react'
const Demo = () => {
const [clientList, setClientList] = useState({}) // 列表数据
const [envTag, setEnvTag] = useState('') // 筛选条件
// 获取列表数据
const getClientList = () => {
fetch(`xxxxxxx?envTag={envTag}`).then(res => { // 请求后端接口
setClientList({
...clientList,
[envTag]: data.items || [] // 通过key去存数据
})
})
}
return (
<div>
<div>
{['全部', '测试', '预发布', '生产'].map((i, index) => (
<span onClick={getClientList}>{i}</span>
))}
<div>
<table dataSource={clientList[envTag]}></table> // 通过key去取数据渲染
</div>
)
}
优点
简单方便,适用于一些数量可控的tab切换和select下拉框一些数据量比较少的tab切换比较适用
缺点
需要提前设置好数据格式,需要一个唯一的key来标识不同的数据
如果数据量比较多,会导致对象里面的属性比较多,不好维护
const Demo = () => {
const [key, setKey] = useState('')
const [clientList, setClientList] = useState({}) // 列表数据
useEffect(() => {
let = fetchId;
const currentId = fetchId;
// 发起一个网络请求
fetch({
key
}).then((res) => {
if (fetchId !== currentId) {
console.error('时序不一致不做任何处理');
} else {
console.log('正常请求')
setClientList(res.data)
}
});
// return出一个闭包
return () => {
fetchId += 1;
};
}, [key]);
return (
<div>
<div>
{['全部', '测试', '预发布', '生产'].map((i, index) => (
<span onClick={getClientList}>{i}</span>
))}
<div>
<table dataSource={clientList}></table> // 通过key去取数据渲染
</div>
)
}
在发起下一个请求时,将之前正在 pending 的请求的取消掉,只取最新的一个请求。
理想的结果如图所示:
当task3请求发起时,task1、task2的请求状态还在pending中,那么取消掉之前的请求,只保留task3的请求。
const Demo = () => {
const [clientList, setClientList] = useState([]) // 列表数据
const [envTag, setEnvTag] = useState('') // 筛选条件
// 获取列表数据
let controller
const getClientList = () => {
if (controller) controller.abort()
controller = new AbortController()
fetch(`xxxxxxx?envTag={envTag}`, {signal: controller.signal }).then(res => {
setClientList(items || [])
controller = null
})
}
return (
<div>
<div>
{['全部', '测试', '预发布', '生产'].map((i, index) => (
<span onClick={getClientList}>{i}</span>
))}
<div>
<table dataSource={clientList}></table> // 通过key去取数据渲染
</div>
)
}
XMLHttpRequest.abort()
cancelToken 的 API 提供完成
这里的取消并不是真正意义上的中断请求,请求还是会正常发出,只是前端不会处理其响应而已。
方案 | 用key独立纯粹数据 | React Hook + 闭包 | 取消请求 |
结合我们具体项目情况,目前我们项目主要采用的是取消请求和react hooks 结合闭包去解决,取消请求效果如下图所示:
react hook + 闭包解决方案效果图:
总结起来就是只要你多次频繁触发同一个动作,多次调用了同一个接口,这个时候就要考虑状态的竞态问题。解决竞态问题的主要思路时手动控制异步结果的顺序,来保证页面渲染的数据正常除了以上例子中的竞态问题,还有如下场景需要考虑竞态问题:
遇到以上的这些竞态问题时,我们也可以用防抖和节流来控制触发异步请求的频率,增强用户体验。关于异步请求的竞态问题,除了上述提到的一些方法外,我们还可以寻找更好更优雅的方式去处理这类情况,比如响应式编程、亦或者函数式编程中的 IO functor 等。对异步的掌控也许还需要我们了解 JavaScript 事件循环、任务队列等知识。
👇长按二维码进天际官方交流群👇
解锁