什么是竞态问题

竞态问题,又叫竞态条件(race condition),它旨在描述一个系统或者进程的输出依赖于不受控制的事件出现顺序或者出现时机。

此词源自于两个信号试着彼此竞争,来影响谁先输出。

简单来说,竞态问题出现的原因是无法保证异步操作的完成会按照他们开始时同样的顺序。举个🌰:

  • 有一个分页列表,快速地切换第二页,第三页;
  • 先后请求 data2 与 data3,分页器显示当前在第三页,并且进入 loading;
  • 但由于网络的不确定性,先发出的请求不一定先响应,所以有可能 data3 比 data2 先返回;
  • 在 data2 最终返回后,分页器指示当前在第三页,但展示的是第二页的数据。

这就是竞态条件,在前端开发中,常见于搜索,分页,选项卡等切换的场景。

那么如何解决竞态问题呢?在以上这些场景中,我们很容易想到:

当发出新的请求时,取消掉上次请求即可。

而我发现意识到问题是在轮询请求的时候,发现页面出现一闪而过的不协调

取消过期请求

XMLHttpRequest 取消请求

XMLHttpRequest(XHR)是一个内建的浏览器对象,它允许使用 JavaScript 发送 HTTP 请求。

如果请求已被发出,可以使用 abort() 方法立刻中止请求。

1
2
3
4
xhr.open('GET', 'https://xxx');
xhr.send();

xhr.abort(); // 取消请求

fetch API 取消请求

fetch 号称是 AJAX 的替代品,出现于 ES6,它也可以发出类似 XMLHttpRequest 的网络请求。

主要的区别在于 fetch 使用了 promise,要中止 fetch 发出的请求,需要使用 AbortController

1
2
3
4
5
6
7
8
9
const controller = new AbortController();
const signal = controller.signal;

fetch('/xxx', {
signal,
}).then(function(response) {
//...
});
controller.abort(); // 取消请求

相比原生 API,大多项目都会选择 axios 进行请求。

axios 取消请求

axios 是一个 HTTP 请求库,本质是对原生 XMLHttpRequest 的封装后基于 promise 的实现版本,因此 axios 请求也可以被取消。

可以利用 axios 的 CancelToken API 取消请求。

1
2
3
4
5
6
7
8
9
const source = axios.CancelToken.source();

axios.get('/xxx', {
cancelToken: source.token
}).then(function (response) {
// ...
});

source.cancel() // 取消请求

所以我们在处理请求错误时,需要判断 error 是否是 cancel 导致的,避免与常规错误一起处理。

1
2
3
4
5
6
7
8
9
axios.get('/xxx', {
cancelToken: source.token
}).catch(function(err) {
if (axios.isCancel(err)) {
console.log('Request canceled', err.message);
} else {
// 处理错误
}
});

但 cancelToken 从 v0.22.0 开始已被 axios 弃用。原因是基于实现该 API 的提案 cancelable promises proposal 已被撤销。

v0.22.0 开始,axios 支持以 fetch API 方式的 AbortController 取消请求

1
2
3
4
5
6
7
8
9
const controller = new AbortController();

axios.get('/xxx', {
signal: controller.signal
}).then(function(response) {
//...
});

controller.abort() // 取消请求

同样,在处理请求错误时,也需要判断 error 是否来自 cancel。

apisauce 取消请求

1
2
3
4
5
6
7
8
import { CancelToken } from 'apisauce'

const source = CancelToken.source()
const api = create({ baseURL: 'github.com' })
api.get('/users', {}, { cancelToken: source.token })

// To cancel request
source.cancel()

每天记录一点,进步多一点点!