<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          如何防止重復(fù)發(fā)送ajax請求

          共 9115字,需瀏覽 19分鐘

           ·

          2020-12-04 21:14

          作者 | 周浪

          背景

          先來說說重復(fù)發(fā)送ajax請求帶來的問題

          • 場景一:用戶快速點擊按鈕,多次相同的請求打到服務(wù)器,給服務(wù)器造成壓力。如果碰到提交表單操作,而且恰好后端沒有做兼容處理,那么可能會造成數(shù)據(jù)庫中插入兩條及以上的相同數(shù)據(jù)
          • 場景二:用戶頻繁切換下拉篩選條件,第一次篩選數(shù)據(jù)量較多,花費的時間較長,第二次篩選數(shù)據(jù)量較少,請求后發(fā)先至,內(nèi)容先顯示在界面上。但是等到第一次的數(shù)據(jù)回來之后,就會覆蓋掉第二次的顯示的數(shù)據(jù)。篩選結(jié)果和查詢條件不一致,用戶體驗很不好

          常用解決方案

          為了解決上述問題,通常會采用以下幾種解決方案

          • 狀態(tài)變量

            發(fā)送ajax請求前,btnDisable置為true,禁止按鈕點擊,等到ajax請求結(jié)束解除限制,這是我們最常用的一種方案但該方案也存在以下弊端:

            • 與業(yè)務(wù)代碼耦合度高
            • 無法解決上述場景二存在的問題
          • 函數(shù)節(jié)流和函數(shù)防抖

            固定的一段時間內(nèi),只允許執(zhí)行一次函數(shù),如果有重復(fù)的函數(shù)調(diào)用,可以選擇使用函數(shù)節(jié)流忽略后面的函數(shù)調(diào)用,以此來解決場景一存在的問題也可以選擇使用函數(shù)防抖忽略前面的函數(shù)調(diào)用,以此來解決場景二存在的問題該方案能覆蓋場景一和場景二,不過也存在一個大問題:

            • wait time是一個固定時間,而ajax請求的響應(yīng)時間不固定,wait time設(shè)置小于ajax響應(yīng)時間,兩個ajax請求依舊會存在重疊部分,wait time設(shè)置大于ajax響應(yīng)時間,影響用戶體驗。總之就是wait time的時間設(shè)定是個難題

          請求攔截和請求取消

          作為一個成熟的ajax應(yīng)用,它應(yīng)該能自己在pending過程中選擇請求攔截和請求取消

          • 請求攔截

            用一個數(shù)組存儲目前處于pending狀態(tài)的請求。發(fā)送請求前先判斷這個api請求之前是否已經(jīng)有還在pending的同類,即是否存在上述數(shù)組中,如果存在,則不發(fā)送請求,不存在就正常發(fā)送并且將該api添加到數(shù)組中。等請求完結(jié)后刪除數(shù)組中的這個api。

          • 請求取消

            用一個數(shù)組存儲目前處于pending狀態(tài)的請求。發(fā)送請求時判斷這個api請求之前是否已經(jīng)有還在pending的同類,即是否存在上述數(shù)組中,如果存在,則找到數(shù)組中pending狀態(tài)的請求并取消,不存在就將該api添加到數(shù)組中。然后發(fā)送請求,等請求完結(jié)后刪除數(shù)組中的這個api

          實現(xiàn)

          接下來介紹一下本文的主角 axioscancel token(查看詳情)。通過axioscancel token,我們可以輕松做到請求攔截和請求取消

          const CancelToken = axios.CancelToken;const source = CancelToken.source();
          axios.get('/user/12345', { cancelToken: source.token}).catch(function (thrown) { if (axios.isCancel(thrown)) { console.log('Request canceled', thrown.message); } else { // handle error }});
          axios.post('/user/12345', { name: 'new name'}, { cancelToken: source.token})
          // cancel the request (the message parameter is optional)source.cancel('Operation canceled by the user.');

          官網(wǎng)示例中,先定義了一個 const CancelToken = axios.CancelToken,定義可以在axios源碼axios/lib/axios.js目錄下找到

          // Expose Cancel & CancelTokenaxios.Cancel = require('./cancel/Cancel');axios.CancelToken = require('./cancel/CancelToken');axios.isCancel = require('./cancel/isCancel');

          示例中調(diào)用了axios.CancelToken的source方法,所以接下來我們再去axios/lib/cancel/CancelToken.js目錄下看看source方法

          /** * Returns an object that contains a new `CancelToken` and a function that, when called, * cancels the `CancelToken`. */CancelToken.source = function source() {  var cancel;  var token = new CancelToken(function executor(c) {    cancel = c;  });  return {    token: token,    cancel: cancel  };};

          source方法返回一個具有tokencancel屬性的對象,這兩個屬性都和CancelToken構(gòu)造函數(shù)有關(guān)聯(lián),所以接下來我們再看看CancelToken構(gòu)造函數(shù)

          /** * A `CancelToken` is an object that can be used to request cancellation of an operation. * * @class * @param {Function} executor The executor function. */function CancelToken(executor) {  if (typeof executor !== 'function') {    throw new TypeError('executor must be a function.');  }
          var resolvePromise; this.promise = new Promise(function promiseExecutor(resolve) { resolvePromise = resolve; });
          var token = this; executor(function cancel(message) { if (token.reason) { // Cancellation has already been requested return; }
          token.reason = new Cancel(message); resolvePromise(token.reason); });}

          所以souce.token是一個CancelToken的實例,而source.cancel是一個函數(shù),調(diào)用它會在CancelToken的實例上添加一個reason屬性,并且將實例上的promise狀態(tài)resolve掉

          官網(wǎng)另一個示例

          const CancelToken = axios.CancelToken;let cancel;
          axios.get('/user/12345', { cancelToken: new CancelToken(function executor(c) { // An executor function receives a cancel function as a parameter cancel = c; })});
          // cancel the requestcancel();

          它與第一個示例的區(qū)別就在于每個請求都會創(chuàng)建一個CancelToken實例,從而它擁有多個cancel函數(shù)來執(zhí)行取消操作

          我們執(zhí)行axios.get,最后其實是執(zhí)行axios實例上的request方法,方法定義在axios\lib\core\Axios.js

          Axios.prototype.request = function request(config) {  ...  // Hook up interceptors middleware  var chain = [dispatchRequest, undefined];  var promise = Promise.resolve(config);
          this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { chain.unshift(interceptor.fulfilled, interceptor.rejected); });
          this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { chain.push(interceptor.fulfilled, interceptor.rejected); });
          while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); }
          return promise;};

          request方法返回一個鏈式調(diào)用的promise,等同于

          Promise.resolve(config).then('request攔截器中的resolve方法', 'request攔截器中的rejected方法').then(dispatchRequest, undefined).then('response攔截器中的resolve方法', 'response攔截器中的rejected方法')

          在閱讀源碼的過程中,這些編程小技巧都是非常值得學(xué)習(xí)的

          接下來看看axios\lib\core\dispatchRequest.js中的dispatchRequest方法

          function throwIfCancellationRequested(config) {  if (config.cancelToken) {    config.cancelToken.throwIfRequested();  }}module.exports = function dispatchRequest(config) {  throwIfCancellationRequested(config);  ...  var adapter = config.adapter || defaults.adapter;  return adapter(config).then()};

          如果是cancel方法立即執(zhí)行,創(chuàng)建了CancelToken實例上的reason屬性,那么就會拋出異常,從而被response攔截器中的rejected方法捕獲,并不會發(fā)送請求,這個可以用來做請求攔截

          CancelToken.prototype.throwIfRequested = function throwIfRequested() {  if (this.reason) {    throw this.reason;  }};

          如果cancel方法延遲執(zhí)行,那么我們接著去找axios\lib\defaults.js中的defaults.adapter

          function getDefaultAdapter() {  var adapter;  if (typeof XMLHttpRequest !== 'undefined') {    // For browsers use XHR adapter    adapter = require('./adapters/xhr');  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {    // For node use HTTP adapter    adapter = require('./adapters/http');  }  return adapter;}
          var defaults = { adapter: getDefaultAdapter()}

          終于找到axios\lib\adapters\xhr.js中的xhrAdapter

          module.exports = function xhrAdapter(config) {  return new Promise(function dispatchXhrRequest(resolve, reject) {    ...    var request = new XMLHttpRequest();    if (config.cancelToken) {      // Handle cancellation      config.cancelToken.promise.then(function onCanceled(cancel) {        if (!request) {          return;        }
          request.abort(); reject(cancel); // Clean up request request = null; }); } // Send the request request.send(requestData); })}

          可以看到xhrAdapter創(chuàng)建了XMLHttpRequest對象,發(fā)送ajax請求,在這之后如果執(zhí)行cancel函數(shù)將cancelToken.promise狀態(tài)resolve掉,就會調(diào)用request.abort(),可以用來請求取消

          解耦

          剩下要做的就是將cancelToken從業(yè)務(wù)代碼中剝離出來。我們在項目中,大多都會對axios庫再做一層封裝來處理一些公共邏輯,最常見的就是在response攔截器里統(tǒng)一處理返回code。那么我們當然也可以將cancelToken的配置放在request攔截器。可參考demo

          let pendingAjax = []const fastClickMsg = '數(shù)據(jù)請求中,請稍后'const CancelToken = axios.CancelTokenconst removePendingAjax = (url, type) => {  const index = pendingAjax.findIndex(i => i.url === url)  if (index > -1) {    type === 'req' && pendingAjax[index].c(fastClickMsg)    pendingAjax.splice(index, 1)  }}
          // Add a request interceptoraxios.interceptors.request.use( function (config) { // Do something before request is sent const url = config.url removePendingAjax(url, 'req') config.cancelToken = new CancelToken(c => { pendingAjax.push({ url, c }) }) return config }, function (error) { // Do something with request error return Promise.reject(error) })
          // Add a response interceptoraxios.interceptors.response.use( function (response) { // Any status code that lie within the range of 2xx cause this function to trigger // Do something with response data removePendingAjax(response.config.url, 'resp') return new Promise((resolve, reject) => { if (+response.data.code !== 0) { reject(new Error('network error:' + response.data.msg)) } else { resolve(response) } }) }, function (error) { // Any status codes that falls outside the range of 2xx cause this function to trigger // Do something with response error Message.error(error) return Promise.reject(error) })

          每次執(zhí)行request攔截器,判斷pendingAjax數(shù)組中是否還存在同樣的url。如果存在,則刪除數(shù)組中的這個api并且執(zhí)行數(shù)組中在pending的ajax請求的cancel函數(shù)進行請求取消,然后就正常發(fā)送第二次的ajax請求并且將該api添加到數(shù)組中。等請求完結(jié)后刪除數(shù)組中的這個api

          let pendingAjax = []const fastClickMsg = '數(shù)據(jù)請求中,請稍后'const CancelToken = axios.CancelTokenconst removePendingAjax = (config, c) => {  const url = config.url  const index = pendingAjax.findIndex(i => i === url)  if (index > -1) {    c ? c(fastClickMsg) : pendingAjax.splice(index, 1)  } else {    c && pendingAjax.push(url)  }}
          // Add a request interceptoraxios.interceptors.request.use( function (config) { // Do something before request is sent config.cancelToken = new CancelToken(c => { removePendingAjax(config, c) }) return config }, function (error) { // Do something with request error return Promise.reject(error) })
          // Add a response interceptoraxios.interceptors.response.use( function (response) { // Any status code that lie within the range of 2xx cause this function to trigger // Do something with response data removePendingAjax(response.config) return new Promise((resolve, reject) => { if (+response.data.code !== 0) { reject(new Error('network error:' + response.data.msg)) } else { resolve(response) } }) }, function (error) { // Any status codes that falls outside the range of 2xx cause this function to trigger // Do something with response error Message.error(error) return Promise.reject(error) })

          每次執(zhí)行request攔截器,判斷pendingAjax數(shù)組中是否還存在同樣的url。如果存在,則執(zhí)行自身的cancel函數(shù)進行請求攔截,不重復(fù)發(fā)送請求,不存在就正常發(fā)送并且將該api添加到數(shù)組中。等請求完結(jié)后刪除數(shù)組中的這個api

          總結(jié)

          axios 是基于 XMLHttpRequest 的封裝,針對 fetch ,也有類似的解決方案 AbortSignal 查看詳情。大家可以針對各自的項目進行選取

          瀏覽 33
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  大鸡巴网免费视频在线 | 天天视频免费入口 | 国产免费一级特黄A片 | 国产一级a爱做片免费☆观看 | 色婷婷精品在线 |