<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

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

    共 9257字,需瀏覽 19分鐘

     ·

    2020-09-05 12:01

    作者 | 周浪

    背景

    先來(lái)說(shuō)說(shuō)重復(fù)發(fā)送ajax請(qǐng)求帶來(lái)的問(wèn)題

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

    常用解決方案

    為了解決上述問(wèn)題,通常會(huì)采用以下幾種解決方案

    • 狀態(tài)變量

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

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

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

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

    請(qǐng)求攔截和請(qǐng)求取消

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

    • 請(qǐng)求攔截

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

    • 請(qǐng)求取消

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

    實(shí)現(xiàn)

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

    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)示例中,先定義了一個(gè) 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方法,所以接下來(lái)我們?cè)偃?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">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方法返回一個(gè)具有tokencancel屬性的對(duì)象,這兩個(gè)屬性都和CancelToken構(gòu)造函數(shù)有關(guān)聯(lián),所以接下來(lái)我們?cè)倏纯?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">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是一個(gè)CancelToken的實(shí)例,而source.cancel是一個(gè)函數(shù),調(diào)用它會(huì)在CancelToken的實(shí)例上添加一個(gè)reason屬性,并且將實(shí)例上的promise狀態(tài)resolve掉

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

    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();

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

    我們執(zhí)行axios.get,最后其實(shí)是執(zhí)行axios實(shí)例上的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方法返回一個(gè)鏈?zhǔn)秸{(diào)用的promise,等同于

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

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

    接下來(lái)看看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實(shí)例上的reason屬性,那么就會(huì)拋出異常,從而被response攔截器中的rejected方法捕獲,并不會(huì)發(fā)送請(qǐng)求,這個(gè)可以用來(lái)做請(qǐng)求攔截

    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對(duì)象,發(fā)送ajax請(qǐng)求,在這之后如果執(zhí)行cancel函數(shù)將cancelToken.promise狀態(tài)resolve掉,就會(huì)調(diào)用request.abort(),可以用來(lái)請(qǐng)求取消

    解耦

    剩下要做的就是將cancelToken從業(yè)務(wù)代碼中剝離出來(lái)。我們?cè)陧?xiàng)目中,大多都會(huì)對(duì)axios庫(kù)再做一層封裝來(lái)處理一些公共邏輯,最常見的就是在response攔截器里統(tǒng)一處理返回code。那么我們當(dāng)然也可以將cancelToken的配置放在request攔截器??蓞⒖糳emo

    let pendingAjax = []const fastClickMsg = '數(shù)據(jù)請(qǐng)求中,請(qǐng)稍后'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ù)組中的這個(gè)api并且執(zhí)行數(shù)組中在pending的ajax請(qǐng)求的cancel函數(shù)進(jìn)行請(qǐng)求取消,然后就正常發(fā)送第二次的ajax請(qǐng)求并且將該api添加到數(shù)組中。等請(qǐng)求完結(jié)后刪除數(shù)組中的這個(gè)api

    let pendingAjax = []const fastClickMsg = '數(shù)據(jù)請(qǐng)求中,請(qǐng)稍后'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ù)進(jìn)行請(qǐng)求攔截,不重復(fù)發(fā)送請(qǐng)求,不存在就正常發(fā)送并且將該api添加到數(shù)組中。等請(qǐng)求完結(jié)后刪除數(shù)組中的這個(gè)api

    總結(jié)

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

    ??愛心三連擊

    1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的在看是我創(chuàng)作的動(dòng)力。

    2.關(guān)注公眾號(hào)程序員成長(zhǎng)指北,回復(fù)「1」加入Node進(jìn)階交流群!「在這里有好多 Node 開發(fā)者,會(huì)討論 Node 知識(shí),互相學(xué)習(xí)」!

    3.也可添加微信【ikoala520】,一起成長(zhǎng)。


    “在看轉(zhuǎn)發(fā)”是最大的支持

    瀏覽 49
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)
    評(píng)論
    圖片
    表情
    推薦
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    影音先锋成人AV | 大香蕉伊人m | 国内一级免费黄色视频在线网展览器的封 | 久久视频黄色 | 国产女孩骚逼AV重口免费大全 |