异步操作对象应该提供可取消操作
对于异步操作如:异步计算、网络请求、定时器等操作。在条件变更以后,需要将上一次变更触发的异步操作中断抛弃、重新触发新的异步操作。未取消失效的异步操作可能导致几个问题:
- 数据请求/参数变更,数据被错误覆盖。第二个请求返回后,上一次请求迟迟返回将数据覆盖为旧数据。
- 未取消的异步操作,引发后续监听的处理函数执行。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
| 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
| 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() { 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(); } } }
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) => { }, (err) => { }) }
|
总结
模块封装过程中涉及到异步/尤其是包含网络请求等场景,都应该提供中止异步,超时中止等基础能力。模块复用和健壮程序需要更多考虑到特殊场景(取消过期请求;中止超时的计算等等)。