为异步操作对象提供可取消操作

异步操作对象应该提供可取消操作

对于异步操作如:异步计算、网络请求、定时器等操作。在条件变更以后,需要将上一次变更触发的异步操作中断抛弃、重新触发新的异步操作。未取消失效的异步操作可能导致几个问题:

  • 数据请求/参数变更,数据被错误覆盖。第二个请求返回后,上一次请求迟迟返回将数据覆盖为旧数据。
  • 未取消的异步操作,引发后续监听的处理函数执行。Promise 监听的 resolve。当然 reject 中也应该考虑到Promise 操作被取消异常。

AbortController 与 AbortSignal API

AbortController 用于控制 AbortSignal ,AbortSignal 是可取消的信号对象。
AbortSignal 对象不可以直接实例化,但提供了两个静态API方法返回 AbortSignal 实例【AbortSignal.abort() 返回一个终止的的AbortSignal 实例; AbortSignal.timeout(tims) 返回一个自动终止的AbortSignal 实例; 】。 AbortController 内部在构造函数中实例化一个 AbortSignal 对象, 并支持 abort 函数api控制是否终止。

取消fetch请求封装
当连续触发请求,参数变更等原因下。将上一次的过期请求取消掉(查询参数已经变更,只等待最新的请求)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class HasSignalFetch {
private signal: AbortController;
constructor() {
this.signal = new AbortController();
}

fetch(input: RequestInfo | URL, init: RequestInit | undefined) {
this.signal = new AbortController();
return fetch(input, {
...(init || {}),
signal: this.signal.signal,
});
}

abort() {
if (this.signal) {
this.signal.abort();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// UI 中使用
export default function App() {
const [sendRe, setSendRe] = useState<HasSignalFetch | undefined>();

const handleSendHtpp = () => {

// 取消上次请求
if (sendRe) {
sendRe.abort();
}

// 生成新的请求并发送
const sendIns = new HasSignalFetch();
setSendRe(sendIns);

sendIns
.fetch("https://8q47rs.csb.app", {
method: "GET",
})
.then(
(res) => {
console.log(res, "请求成功!!");
},
(err) => {
console.warn(err, "请求错误!");
}
);
};

return (
<div className="App">
<button onClick={handleSendHtpp}>发送请求</button>
</div>
);
}

超时场景

接口超时设定一般都来自 axios 等封装库,这里使用 AbortSignal.timeout 快速实现一个超时取消请求功能。另一种方式是使用 setTimeout 定时器,在定时器回调中AbortController 实例的abort函数手动中止。

1
2
3
4
// 超时时间 10s ,超过10s 自动取消
fetch('<https://xxx.com>', {
signal: AbortSignal.timeout(1000*10)
})

使用定时器回调中取消:

1
2
3
4
5
6
7
8
9
10
11
12
13
function handleClick() {
const controller = new AbortController();

// 超时取消
setTimeout(()=>{
controller.abort();
}}, 10*1000);

fetch('<https://xxx.com>', {
signal: controller.signal
});
}

AbortSignal.timeout(tims) 返回的信号对象实例计算时间从调用开始计时,并不是 fetch 或异步开始时计时。参考如下代码使用误区:

1
2
3
4
5
6
7
8
9
10
11
function handleClick() {
// 倒计时 10s 从timeout函数调用开始计算
const signal = AbortSignal.timeout(8*1000);

setTimeout(()=>{
// 请求永远超时
fetch('<https://xxx.com>', {
signal
});
}, 10*1000);
}

复合场景: 超时自动中止和其他条件自定义中止
接口如果超过10s还未请求回来中止掉,参数变更请求取消。两个条件均需要取消异步或者请求时,AbortSignal.timeout 就不适用了,只能使用 AbortController 实例 + setTimeout 定时器进行实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class HasSignalFetch {
private signal: AbortController;
private timeout: number;
constructor(timeout?: number) {
this.signal = new AbortController();
this.timeout = timeout;
}

// 超时自动中止
private timeoutAutoAbort() {
if (this.timeout > 0) {
setTimeout(() => {
if (this.signal && !this.signal.signal.aborted) {
this.signal.abort();
}
}, this.timeout);
}
}

fetch(input: RequestInfo | URL, init: RequestInit | undefined) {
this.signal = new AbortController();

this.timeoutAutoAbort();
return fetch(input, {
...(init || {}),
signal: this.signal.signal,
});
}

abort() {
if (this.signal) {
this.signal.abort();
}
}
}


// use case
function handleClick() {
const sendIns = new HasSignalFetch(10 * 1000);

Promise.all([
sendIns.fetch('https://xxx.com/a', { method: 'POST' }),
sendIns.fetch('https://xxx.com/b', { method: 'GET' })
]).then((resArr) => {
// resArr 处理得到的数据
}, (err) => {
// 处理失败的场景,包含超时被终止,手动中止,请求错误等等!
})
}

总结

模块封装过程中涉及到异步/尤其是包含网络请求等场景,都应该提供中止异步,超时中止等基础能力。模块复用和健壮程序需要更多考虑到特殊场景(取消过期请求;中止超时的计算等等)。