什么是竞态?如何优雅的解决接口响应乱序的问题?如何取消fetch()触发的网络请求?一个高阶函数就能搞定
什么是竞态
首先,什么是异步任务的竞态?前端遇到竞态的场景与后端略有不同,处理方式也各不相同,但是发生竞态的根源是一致的,那就是异步任务的不可控和不可预测。请看图

这时就出现了状态的竞争,因为很有可能你在task5响应时渲染的结果,很快就被task4的响应覆盖了,又很快被task3的响应覆盖了。
按照常规的业务设计,我们应该只保留task5的查询结果。这就需要在新的task开始时,结束先前已经发起的竞争task
理想的结果如图:

想要实现以上效果,可以利用XMLHttpRequest.abort() 、或者借助Axios等三方库来实现。使用fetch发起的网络请求是无法在客户端终止的。但是我们可以换个思路,封装一个可被打断的Promise。
可被打断的Promise
在业务开发中,一个可被打断的Promise有时能起到事半功倍的作用。
function task() {
let cancel;
const cancelablePromise = new Promise(function (resolve, reject) {
cancel = reject
// do sth.
do_sth()
})
cancelablePromise.cancel = cancel;
return cancelablePromise;
}
const p = task();
p.cancel();
需要注意的是,do_sth() 函数的执行并没有被打断。被打断的只是const p ,它是一个promise。如果在调用p.cancel()之前,p的状态已经变成了resolved,那么p.cancel不起任何作用。
用高阶函数隔离脏操作
如上所述,在外部打断promise是一种脏操作,使用时必须极为谨慎。
接下来,介绍如何通过高阶函数的封装,把脏操作隔离到一个相对安全的空间。来一个野鸡变凤凰。
function autoCancel(fn, onCancelErr = null) {
const cancelSymbol = Symbol('cancel');
let running;
return async function r(...args) {
running && running()
running = null;
const result = await Promise.race([fn(...args), (function () {
return new Promise(function (resolve) {
running = resolve.bind(undefined, cancelSymbol)
})
})()]);
if (result === cancelSymbol) {
if (onCancelErr) {
throw onCancelErr
}
return null;
}
return result;
}
}
如上,20行代码,就实现了一个高阶函数,内部封印了一头能量巨大的野兽。
原理很简单,利用了Promise.race的特性,通过抢先resolve第二个promise来实现打断。当上一个任务还没有完成,而新的任务又被创建时,直接返回一个undefined(或者抛错onCancelErr)。
下面,就来看一下如何驾驭这头野兽:
const queryList = autoCancel(async function(param) {
const resp = await fetch(`/query?name=${param}`);
return await resp.json();
}, new Error('canceled'));
try {
const result = await queryList('helo');
} catch(e) {
// do sth
}
同样需要注意的是,被中断的只是一个promise,真正的网络请求并没有被终止,它还是会正常返回,只是返回结果被忽略了。
例子
下面是一个完整的例子:


如上图,task5最后开始却最先完成。应该忽略task1-4的执行结果,只保留task5的返回值。
最后
如果我的分享对你有帮助,可以关注我的公众号DevStudio, 第一时间查收优质技术分享。
