什么是竞态问题
竞态问题,又叫竞态条件(race condition),它旨在描述一个系统或者进程的输出依赖于不受控制的事件出现顺序或者出现时机。此词源自于两个信号试着彼此竞争,来影响谁先输出。
简单来说,竞态问题出现的原因是无法保证异步操作的完成会按照他们开始时同样的顺序。
● 有一个分页列表,快速地切换第二页,第三页;
● 先后请求 data2 与 data3,分页器显示当前在第三页,并且进入 loading;
● 但由于网络的不确定性,先发出的请求不一定先响应,所以有可能 data3 比 data2 先返回;
● 在 data2 最终返回后,分页器指示当前在第三页,但展示的是第二页的数据。
这就是竞态条件,在前端开发中,常见于搜索,分页,选项卡等切换的场景。
那么如何解决竞态问题呢?在以上这些场景中,我们很容易想到:
● 当发出新的请求时,取消掉上次请求即可
● 接口返回增加标识
取消过期请求
- XMLHttpRequest 取消请求 XMLHttpRequest(XHR)是一个内建的浏览器对象,它允许使用 JavaScript 发送 HTTP 请求。如果请求已被发出,可以使用 abort() 方法立刻中止请求。
constxhr=newXMLHttpRequest(); xhr.open('GET','https://xxx'); xhr.send(); xhr.abort();// 取消请求
- fetch API 取消请求 fetch 号称是 AJAX 的替代品,出现于 ES6,它也可以发出类似 XMLHttpRequest 的网络请求。主要的区别在于 fetch 使用了 promise,要中止 fetch 发出的请求,需要使用 AbortController。
constcontroller =newAbortController(); constsignal = controller.signal; fetch('/xxx', { signal, }).then(function(response){ //... }); controller.abort();// 取消请求
相比原生 API,大多项目都会选择 axios 进行请求。
- axios 取消请求 axios 是一个 HTTP 请求库,本质是对原生 XMLHttpRequest 的封装后基于 promise 的实现版本,因此 axios 请求也可以被取消。可以利用 axios 的 CancelToken API 取消请求。
constsource = axios.CancelToken.source(); axios.get('/xxx', { cancelToken: source.token }).then(function(response){ // ... }); source.cancel()// 取消请求
在 cancel 时,axios 会在内部调用promise.reject()与xhr.abort()。
可取消的 promise
原生 promise 并不支持 cancel,但 cancel 对于异步操作来说又是个很常见的需求。
functiongetAbortController(){ varabortSignal = getAbortSignal(); varabort =function(reason){ if(abortSignal.aborted) {return; } abortSignal.aborted =true; abortSignal.dispatchEvent(reason);// Different from AbortSignal }; return{ signal: abortSignal, abort: abort }; }/** * Custom AbortSignal * *@return{Object}abortSignal */functiongetAbortSignal(){ varabortSignal = { aborted:false, onabort:null }; abortSignal.dispatchEvent =function(event){ if("function"===typeofabortSignal.onabort) { abortSignal.onabort(event); } }; returnabortSignal; } classCanAbortPromiseextendsPromise{ constructor(executor, abortController = getAbortController()) { super((resolve, reject) =>{ executor(resolve, reject, abortController.signal); }); this.abortController = abortController; } then (onFulfilled, onRejected) { returnnewCanAbortPromise((resolve, reject, signal) =>{ constonSettled =(status, value, callback) =>{ if("function"===typeofcallback) { value = callback(value); if(valueinstanceofCanAbortPromise) { Object.assign(signal, value.abortController.signal); } returnresolve(value); } "resolved"=== status && resolve(value); "rejected"=== status && reject(value); } super.then( value=>onSettled("resolved", value, onFulfilled), reason => onSettled("rejected", reason, onRejected) ); },this.abortController); } // Equivalent to this.then(undefined, onRejected) // catch (onRejected) {} abort (reason) { returnnewCanAbortPromise((resolve, reject) =>{ Promise.resolve().then(()=>{ this.abortController.abort(reason); this.then(resolve, reject); }); },this.abortController); } staticall (promises) { returnnewCanAbortPromise((resolve, reject, signal) =>{ setPromisesAbort(promises, signal); Promise.all(promises).then(resolve, reject); }); } staticrace (promises) { returnnewCanAbortPromise((resolve, reject, signal) =>{ setPromisesAbort(promises, signal); Promise.race(promises).then(resolve, reject); }); } };/** * Set promises abort * @param {Array} promises - list of promise * @param {Object} signal - abort signal */functionsetPromisesAbort(promises, signal){ signal.onabort =reason=>{ promises.forEach((promise) =>{ if(promiseinstanceofcanAbortPromise) { promise.abort(reason).catch(error=>error); } }); } } constpromise1 =newCanAbortPromise((resolve, reject, signal) =>{ setTimeout(resolve,1000,"resolve"); signal.onabort = reject; }); constpromise2 = promise1.then(value=>{ console.log(value);// no execute }).catch(reason=>{ console.log(reason);// output "abort promise" }); promise1.abort("abort promise"); // or promise2.abort("abort promise");
取消」和「忽略」的比较
-
「取消」更实际 如果请求被「取消」了没有到达服务端,那么可以一定程度减轻服务的压力。
但是取消请求也依赖底层的请求 API,比如 XMLHttpRequest 需要用 abort,而 fetch API 和 axios 需要用 AbortController。 -
「忽略」更通用 而「忽略」的方式,不依赖请求的 API,更加通用,更容易抽象和封装。本质上所有的异步方法都可以使用 abort 来忽略过期的调用。
一个更实际,一个更通用,两者的使用需要根据具体场景来权衡。
本文采摘于网络,不代表本站立场,转载联系作者并注明出处:https://www.iotsj.com//kuaixun/3132.html