什么是竞态问题
竞态问题,又叫竞态条件(race condition),描述了一个系统或进程的输出依赖于不受控制的事件出现顺序或时机。在前端开发中是一个比较常见的问题,尤其是处理异步请求时。
🌰:当用户频繁的切换一个表格的分页时,由于网络的不稳定性,更早发起的请求的响应时间可能比晚请求的响应时间更晚,可能导致页面数据错误的问题。
如何解决
这里提供几种思路
I.避免同一个请求多次发送
如果采用这种方式,我们可以设置一些请求标识符,如loading,来标识我们我们正在发送请求。发送请求的过程中我们让页面出现加载动画、或者禁止切换等方式来避免多次发送同一个请求。也可以设置防抖节流来控制用户发送请求的频次。
这种方式比较简单粗暴,而且对用户的体验来说稍有影响,但却是一个比较简单有效的解决方法。
II.忽略过期请求
我们可以设置一个请求id,每次发送请求生成一个新的请求id,在响应时判断请求id是否一致来处理请求响应。
这里采用一个delay函数模拟请求延时,设置7个模拟请求,每个请求间隔100ms。采用设置请求ID的方式来忽略过期的请求。
function delay(msDelay) {
return new Promise((resolve) => setTimeout(resolve, msDelay));
}
// 全局变量记录相关信息
let latestRequestId = null;
let completedRequests = 0;
async function fetchData(msDelay, requestId) {
try {
// 更新最新请求id为当前请求id
latestRequestId = requestId;
console.log(`请求开始 - ID: ${requestId}, 预计延迟: ${msDelay}ms`);
// 模拟请求延迟
await delay(msDelay);
// 如果最新请求id与当前请求id一致,处理结果
if (requestId === latestRequestId) {
console.log(`请求 ${requestId} 是最新的. 处理最新结果.`);
} else {
console.log(`请求 ${requestId} 已过时. 忽略结果.`);
}
// 更新已完成请求计数
completedRequests++;
if (completedRequests === numRequests && endTime === null) {
endTime = Date.now();
console.log(`所有请求已完成. 总请求数: ${numRequests}, 最后一个请求ID: ${latestRequestId}`);
}
} catch (error) {
console.error(`请求出现错误:`, error);
}
}
const numRequests = 7;
const delays = [3000, 2000, 3000, 1000, 300, 100, 1000];
for (let i = 0; i < numRequests; i++) {
setTimeout(() => {
fetchData(delays[i], i)
}, i * 100)
}
处理ID为6也就是最后一个发起的请求之外,其他的请求都被忽略了。
III.取消重复请求
还没有完成的HTTP请求是可以被取消的。那么当下一个请求发起时,上一个请求还未完成,我们直接取消掉也可以解决竞态问题。
在日常开发中我们大多数都是使用AXIOS来进行HTTP请求,这里提供AXIOS取消请求的示例。
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// 取消请求
controller.abort()
当然AXIOS还有通过CancelToken的方式来取消请求,但是在新版本中已经被标记弃用了。我们应尽量使用AbortController来取消请求。这里也给出CancelToken的示例。
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});
// 取消请求
cancel();