background

Let’s start with the problem of repeatedly sending Ajax requests

  • Scenario 1: A user clicks the button quickly and sends the same request to the server for several times, which causes pressure on the server. If you encounter a submit form operation that is not compatible on the back end, you may insert two or more pieces of the same data into the database
  • Scenario 2: Users frequently change the drop-down filter criteria. The first filter requires a large amount of data and takes a long time. The second filter requires a small amount of data. But when the first one comes back, it overwrites the second one. The filtering results are inconsistent with the query conditions, resulting in poor user experience

Common Solutions

To solve the above problems, the following solutions are commonly used

  • State variables

    BtnDisable is set to true before sending the Ajax request, disables button clicking, and releases the restriction when the Ajax request ends. This is the most common scenarioHowever, the scheme also has the following disadvantages:

    • High degree of coupling with business code
    • The problem in scenario 2 cannot be solved
  • Function throttling and function stabilization

    Only one function is allowed to be executed at a fixed time. If there are repeated function calls, you can choose to use function throttling to ignore subsequent function calls to solve the problem in scenario 1Alternatively, you can use function stabilization to ignore the previous function call to solve the problem in Scenario 2This scheme covers scenario 1 and scenario 2, but there is one big problem:

    • The wait time is a fixed time, but the ajax request response time is not fixed. Therefore, the Wait time is smaller than the Ajax response time, and the two Ajax requests still overlap, and the wait time is longer than the Ajax response time, affecting the user experience. Wait time is a difficult time to set

Request interception and request cancellation

As a mature Ajax application, it should be able to choose between request interception and request cancellation in the pending process itself

  • Request to intercept

    An array is used to store requests that are currently pending. Before sending a request, check whether the API has a pending class before the request, that is, whether it exists in the above array. If so, do not send the request. If not, send it normally and add the API to the array. Remove the API from the array after the request is complete.

  • The request to cancel

    An array is used to store requests that are currently pending. When sending the request, determine whether there is any other class of the API that is still pending before the request, that is, whether it exists in the above array. If so, find the request in the array and cancel it. If not, add the API to the array. Then send the request, and when the request is complete, remove the API from the array

implementation

Here’s axios’ Cancel token, the subject of this article (see more). Request interception and request cancellation are easy to do with axios’ Cancel 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.');Copy the code

CancelToken = axios.CancelToken CancelToken = axios.CancelToken CancelToken = axios.CancelToken

// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');Copy the code

Example invokes the axios. CancelToken source method, and so here we are again to axios/lib/cancel/CancelToken js directory to see the source method

/**
 * 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
  };
};Copy the code

The source method returns an object with token and Cancel attributes, both of which are associated with the CancelToken constructor, so let’s look at the CancelToken constructor

/**
 * 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);
  });
}Copy the code

So souce.token is an instance of CancelToken, and source.cancel is a function that adds a Reason attribute to the CancelToken instance and resolves the promise state on the instance

Here’s another example

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel functionas a parameter cancel = c; })}); // cancel the request cancel();Copy the code

It differs from the first example in that each request creates an instance of CancelToken, which has multiple cancel functions to perform the cancellation

When we execute axios.get, we actually end up executing the request method on the axios instance, which is defined in 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;
};Copy the code

The request method returns a chained call promise, equivalent to

Promise.resolve(config).then('Resolve method in request interceptor'.'Request interceptor rejected method').then(dispatchRequest, undefined).then('Resolve method in Response interceptor'.'Rejected method in response interceptor')Copy the code

In the process of reading the source code, these programming tips are well worth learning

Next look at the dispatchRequest method in axios\lib\core\dispatchRequest.js

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

If the Cancel method executes immediately and creates the Reason attribute on the CancelToken instance, an exception will be thrown and the Rejected method in the Response interceptor will not send the request. This can be used for request interception

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

If the cancel method is delayed, then we go to the defaults.adapter in axios\lib\defaults.js

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()
}Copy the code

Finally find the xhrAdapter in Axios \lib\ Adapters \xhr.js

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); })}Copy the code

As you can see, the xhrAdapter creates the XMLHttpRequest object, sends the Ajax request, and after that, if the cancel function canceltoken. promise state resolve is cancelled, request.Abort () is called, which can be used to request cancellation

The decoupling

All that remains is to separate cancelToken from the business code. In most projects, we wrap another layer of axios library to handle some common logic, the most common being the uniform handling of the return code in the Response interceptor. We could of course put the cancelToken configuration in the Request interceptor. Refer to the demo

let pendingAjax = []
const fastClickMsg = 'Data request, please hold'
const CancelToken = axios.CancelToken
const 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 interceptor
axios.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 interceptor
axios.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)
  }
)Copy the code

Each time you execute the Request interceptor, determine if the same URL still exists in the pendingAjax array. If so, remove the API from the array and cancel the request by executing the cancel function for the pending Ajax request in the array, then send the second Ajax request normally and add the API to the array. Remove the API from the array after the request is complete

let pendingAjax = []
const fastClickMsg = 'Data request, please hold'
const CancelToken = axios.CancelToken
const 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 interceptor
axios.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 interceptor
axios.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)
  }
)Copy the code

Each time you execute the Request interceptor, determine if the same URL still exists in the pendingAjax array. If it does, it executes its own cancel function to intercept the request, does not repeat the request, does not exist and adds the API to the array. Remove the API from the array after the request is complete

conclusion

Axios is a wrapper based on XMLHttpRequest. AbortSignal is a similar solution for FETCH. You can pick and choose for your own project