由于请求的竞态问题,前端仔喜提了一个bug
在平常的开发过程中,你可能会遇到这样一个bug。
测试:我在测一个输入框搜索的功能时,告诉你通过输入框输入的内容,和最终通过输入内容搜索出来的结果对不上。
前端:我是通过调用后端接口拿到的数据,这明显是后端返回的结果有问题啊,找后端去!
后端:通过Postman
一通自测后说道,结果没问题啊!找前端去!
前端:我来试试看!一顿输入后发现,没问题啊,这bug
我复现不出来啊!
测试:这个bug是偶现的!
前端:一通排查后发现,bug的直接原因是网络问题,根本原因是竞态问题,导致的数据不一致(喜提一个有意思的bug)。
一、bug原因
举个例子:
- 你先输入1,发起请求A,此时请求参数为
{ searchKey: '1' }
; - 你再输入2,发起请求B,此时请求参数为
{ searchKey: '12' }
;
此时由于网络原因,导致先发起的请求A的响应结果比请求B的慢
- 拿到响应B的结果,此时页面先渲染B的响应结果;
- 拿到响应A的结果,此时页面再渲染A的响应结果。
所以最后就会发现,你输入的结果,跟搜索出来的结果对不上。
二、解决方案
要解决它,就需要了解一个知识点,那就是如何取消请求
。我在发起请求B的时候,把请求B取消不就搞定了么!
三、如何取消请求?
1. 原生XMLHttpRequest
如果用的JavaScript
原生的XMLHttpRequest
发起的请求,可以通过调用abort
方法来取消请求。
var xhr = new XMLHttpRequest();
var url = 'http://localhost:3000/data';
var timer;
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
clearTimeout(timer); // 如果在300ms内收到响应,则清除定时器
var response = JSON.parse(xhr.responseText);
console.log(response);
} else {
console.log('Error: ' + xhr.status);
}
};
// 设置定时器,在300ms后取消请求
timer = setTimeout(function() {
xhr.abort();
console.log('Request aborted due to timeout');
}, 300);
// 发起请求
xhr.send();
我这里利用XMLHttpRequest
发起了一个请求,如果300ms
后未拿到响应,我便会调用abort
方法取消请求。
2. fetch
通过fetch
发起的请求,需要通过AbortController
来实现请求的取消,具体步骤如下:
- 先通过
new AbortController
创建一个实例,比如叫controller
, - 通过
controller.signal
拿到一个信号
,然后再发起fetch
请求的时候带上这个信号
,然后这个请求就与这个信号
关联在一起了。 - 通过第2步的关联之后,可以随时通过调用
controller.abort()
取消请求。
具体代码如下,通过fetch
发起请求,还是在300ms
后未拿到响应便取消请求:
// 创建一个 AbortController 实例
const controller = new AbortController();
const signal = controller.signal;
// 设置取消请求的定时器
const timeout = setTimeout(() => {
controller.abort();
console.log('Request aborted due to timeout');
}, 300); // 300ms
// 发起 Fetch 请求
fetch('http://localhost:3000/data', { signal })
.then(response => {
clearTimeout(timeout); // 清除定时器
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Error:', error);
}
});
3、axios
其实平常开发中,发请求最常用的方式还是axios
,它是对原生XMLHttpRequest
的一个封装,让我们用起来更爽。
它是通过CancelToken
来取消请求的,其步骤如下:
- 通过
axios
上的静态属性CancelToken
直接先拿到CancelToken
, - 调用
CancelToken.source()
方法,拿到source
, - 发请求时,通过带上
source.token
,将请求和source
相关联, - 通过第2步的关联之后,可以随时通过调用
source.cancel
取消请求。
完整代码如下:
// 创建一个 CancelToken.source
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
// 发起 Axios 请求并传入 cancel token
axios.get('http://localhost:3000/data', {
cancelToken: source.token
})
.then(response => {
console.log(response.data);
})
.catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
console.error('Error:', thrown);
}
});
// 在需要的时候取消请求
setTimeout(() => {
source.cancel('Request canceled due to timeout');
}, 300); // 300ms
axios底层也是通过
abort
方法来实现的。
四、应用场景
那么取消请求有哪些应用场景呢?
- 连续搜索:那就是上面提到的bug,在连续发起多个请求后,就可能会出现竞态问题,引发bug,
- 大文件上传:比如在做一个大文件上传功能时,用户上传文件后,页面显示了实时进度条以及取消上传按钮,假如用户此时点击了取消按钮,就应该把已经发送但未拿到响应的请求取消掉。
五、小结
上面主要介绍了通过xhr
、fetch
以及axios
发起的请求如何取消,xhr
是通过abort
方法来取消请求,fetch
则是借助了AbortController
类来实现,而axios
虽然表面是借助了CancelToken
来实现,实际底层还是调用xhr.abort()
方法来实现的。