异步请求竞态问题
通过记录全局的唯一id来解决
TIP
核心思路: 记录一个全局id,每次请求触发时id自增,并且将id作为响应数据返回。 在请求完成处理响应数据时,判断全局id跟响应数据返回的id,如果两个值相等则代表此次请求是最新的请求,处理响应数据即可。
tips: 本质上这种方法是放弃了旧的请求响应数据,只处理最新的请求响应数据。
实现
let index = 0
const request = (msg, id) => {
return new Promise(resolve => {
let current = ++index
setTimeout(() => {
resolve({
errno: 0,
msg,
current,
})
}, id * 1000)
})
}
const search = async (params, id) => {
const res = await request(params, id)
if (res.current === index) {
console.log(res, 'request success')
}
}
// test case
search('first req', 0.5)
search('second req', 2)
search('three req', 0.1) // { errno: 0, msg: 'three req', current: 3 } request success
使用Promise和xhr.abort(): Axios中如何解决异步请求竞态问题
TIP
核心思路: 利用promise和XMLHttpRequest的abort方法解决xhr异步请求的竞态问题(本文示例用计时器模拟xhr异步事件)。
每一个异步请求都会返回一个 pending 的 promise ,因此可以在连续的请求中将上一个请求的promise reject, 同时执行XHR对象的abort方法,确保响应的请求是最新的。
那要如何reject上一个promise呢? 可以通过插入一个新的promise(下文称cancelPromise
),在发起请求的promise中(下文称xhrPromise
)来监听cancelPromise的状态(cancelPromise.then)。
同时我们将cancelPromise的控制权(resolve函数)交由到外层,可以由开发者决定何时调用(即手动调用cancel函数的时候)。
那么当cancelPromise的状态被改变时(调用resolve), 在xhrPromise内监听cancelPromise状态的回调函数就会执行(cancelPromise.then), 因此就能reject xhrPromise并且执行取消请求(xhr.abort)
tips: axios中取消xhr请求相关实现
Details
// 错误消息类
class CanceledError {
constructor(message) {
this.message = message
}
}
// 取消请求类
class CancelToken {
constructor(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function')
}
// 记录cancelPromise的resolve,供外层调用可用来改变cancelPromise的状态
let resolvePromise
// cancelPromise
this.promise = new Promise(resolve => {
resolvePromise = resolve
})
let token = this
// 执行executor,并将cancel函数作为参数传入,则外部可以获取到cancel函数(函数内部调用resolvePromise,可改变cancelPromise的状态)
executor(function cancel(message) {
if (token.reason) return
token.reason = new CanceledError(message)
resolvePromise(token.reason)
})
}
static source() {
let cancel
const token = new CancelToken(c => {
cancel = c
})
return {
token,
cancel
}
}
}
// 异步请求包装
function xhrAdapter(url, cancelToken) {
return new Promise((resolve, reject) => {
// 模拟异步请求
let timeId = setTimeout(() => {
resolve(`响应数据: ${url}`)
}, url)
if (cancelToken) {
// 监听cancelPromise回调,回调里reject本次promise,同时取消异步请求;
// tips: 开发者手动调用cancel函数时会执行resolvePromise,然后此处回调执行。
cancelToken.promise.then(reason => {
clearTimeout(timeId)
reject({
reason,
isCancel: true,
})
})
}
})
}
// test case
// 1. 使用new CancelToken()
let cancel
let reqId = 0
function run(id) {
if (cancel) {
cancel(`取消第${++reqId}个请求`)
}
xhrAdapter(id * 1000, new CancelToken(c => {
cancel = c
})).then(res => {
console.log(res, 'request success')
}).catch(err => {
// console.log(err, 'request catch')
if (err.isCancel) {
console.log(err.reason.message, 'request canceled')
} else {
console.log(err, 'request error')
}
})
}
// 2. 使用CancelToken.source()
let source
let reqId = 0
function run(id) {
if (source) {
source.cancel(`取消第${++reqId}个请求`)
}
source = CancelToken.source()
xhrAdapter(id * 1000, source.token).then(res => {
console.log(res, 'request success')
}).catch(err => {
if (err.isCancel) {
console.log(err.reason.message, 'request canceled')
} else {
console.log(err, 'request error')
}
})
}
// 测试同时发起多个请求,仅响应最新的请求
run(1.5)
run(2)
run(1)
fetch和AbortController
TIP
AbortController
接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。
fetch和AbortController实现取消请求的实现及相关概念,可以查看文档
tips: 从 v0.22.0
开始,Axios 支持以 fetch API 方式—— AbortController 取消请求; 并且xhr相关的CancelToken也从v0.22.0开始被弃用。