前言
大家好,我是抹茶。
在日常的工作当中,我们可能会遇到这样的情况,在同样的页面,可能发送了A,B,C请求,这时候,用户希望看到的是C请求的结果,但是可能A请求反应最慢,最后才返回,如果没有留意到这一点,可能给用户展示的就是错误的界面。
竞态问题,又叫竞态条件(race condition),它出现的原因是无法保证异步操作不一定会按他们开始的顺序执行。
针对这样的情况,我们可以用四种方案作处理。
1.闭包
function ajaxControl (cb) {
let cleanup;
function onInvalidate (fn) {
cleanup = fn;
}
const a = (name, time) => {
if (cleanup) cleanup();
cb(onInvalidate, name, time)
}
return a;
}
async function query (onInvalidate, name, time) {
let expired = false;// 请求是否过期
onInvalidate(() => expired = true);
const res = await new Promise((resolve) => {
setTimeout(() => {
const time = new Date().getTime();
const str = new Date().toLocaleTimeString();
console.log('time: ' + time);
console.log('str: ' + str);
resolve({name,time});
}, time)
})
console.log(name);
console.log('expired', expired);
console.log('\n');
if (expired) return;
data = res;
}
let data;
const test = ajaxControl(query);
test('A请求', 3000);
test('B请求', 2000);
test('C请求', 1000);
setTimeout(() =>console.log('data',data),4000)
执行的结果如下
可以看到data最后保存的是最新的请求C的数据。
在执行请求A时,大致的调用栈如下:
当执行请求B的时候,因为cleapup变量已经赋值为请求A调用栈中的函数()=> expired = true,所以请求B执行的时候,请求A中的expired变量已经被修改为true,等A请求返回响应的时候,依据逻辑,expired为true时直接返回,所以总有当前请求会把上一个请求的过期标致设置为true,从而实现了我们的data始终是最新请求的结果。
2.abort()
XMLhttpRequest是支持取消的,根据MDN文档,大致用法如下
var xhr = new XMLHttpRequest(),
method = "GET",
url = "https://developer.mozilla.org/";
xhr.open(method, url, true);
xhr.send();
if (OH_NOES_WE_NEED_TO_CANCEL_RIGHT_NOW_OR_ELSE) {
xhr.abort();
}
如果用fetch,也有AbortController可以取消
const controller = new AbortController();
const signal = controller.signal;
const url = "video.mp4";
const downloadBtn = document.querySelector(".download");
const abortBtn = document.querySelector(".abort");
downloadBtn.addEventListener("click", fetchVideo);
abortBtn.addEventListener("click", () => {
controller.abort();
console.log("Download aborted");
});
function fetchVideo() {
fetch(url, { signal })
.then((response) => {
console.log("Download complete", response);
})
.catch((err) => {
console.error(`Download error: ${err.message}`);
});
}
对于常用的Axios,支持以 fetch API 方式—— AbortController 取消请求
const controller = new AbortController();
async function getOpinionList (time) {
const res = await axios.post('https://xxx', {
signal: controller.signal
})
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, time)
})
return Promise.resolve(res);
}
let res;
function fetchData (name, time) {
controller.abort();// 取消请求
getOpinionList(name, time).then(function(response) {
res = { name };
});
}
fetchData('A请求', 3000);
fetchData('B请求', 2000);
fetchData('C请求', 1000);
setTimeout(() => {
console.log(res);
}, 4000)
请求结果保存的是最后触发的C请求
3.requestId
用特殊字符当成requestId,利用axios的请求拦截器,在请求头上加上唯一标识,本地存储最后请求的唯一标识符。
const axiosInstance = axios.create();
const localStorage = {
lastRequestId: 0,
lastRequestName: undefined
}
let list = [];
// 添加请求拦截器
axiosInstance.interceptors.request.use(config => {
// 生成一个唯一的请求ID,这里使用时间戳作为示例
const requestId = `${Date.now()}`;
// 将requestId添加到请求的headers中
config.headers['X-Request-Id'] = requestId;
config.headers['X-Request-name'] = encodeURIComponent(config.data.name);
// 收集到的三次请求的时间戳是一样的,requestId不能用Date.now(),应该用UUID
list.push(requestId);
// 可以将requestId存储起来,例如在localStorage或全局状态管理中
localStorage['lastRequestId'] = requestId;
localStorage['lastRequestName'] = config.data.name;
// 返回修改后的配置
return config;
}, error => {
// 请求错误处理
return Promise.reject(error);
});
async function getOpinionList (name,time) {
const res = await axiosInstance.post('https://xxx',{name});
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, time)
})
return Promise.resolve(res);
}
let res;
function fetchData (name, time) {
getOpinionList(name, time).then(function(response) {
const rName = decodeURIComponent(response.config.headers['X-Request-name']);
console.log('响应的请求', rName);
if (rName === localStorage['lastRequestName']) {
res = { name };
}
});
}
fetchData('A请求', 3000);
fetchData('B请求', 2000);
fetchData('C请求', 1000);
setTimeout(() => {
console.log(localStorage)
console.log(list)
console.log('存储的结果',res);
}, 4000)
在测试过程中发现,如果单纯用Date.now()当成requestId,则三次请求的时间戳都是一样的,就会导致不准确,所以requestId不能单纯使用时间戳,而是用UUID的形式,测试demo是用了lastRequestName存储唯一标识。
请求结果如
4.时间戳
让后端返回一个时间戳,每次都判断,保证永远只保存最后一次发起的请求的时间戳的数据即可。
function getOpinionList (time) {
return axios.post('https://xxx')
}
let res;
let maxTimestamp = 0;// 记录最后一次请求的时间戳,依旧大小更新
const list = [];
function fetchData (name) {
getOpinionList(name).then(function(response) {
const timestamp = response.data.timestamp;
list.push({ name, timestamp});
if (timestamp > maxTimestamp) {
res = { name ,timestamp};
}
});
}
fetchData('A请求', 5000);
fetchData('B请求', 3000);
fetchData('C请求', 1000);
setTimeout(() => {
console.log('各请求时间戳',list)
console.log('存储结果',res);
}, 6000)
结果如
总结
本文介绍了4种解决竞态条件的处理方案,其中方案一是在Vue3的watch设计中学来的,不失为一种虽然有点绕,但是极为优雅的处理方式,不依赖于第三方插件功能或者是其他的附加内容信息,单单依靠JS的闭包就能实现,相当优雅了。