Axios source code learning

Axios is a Promise-based HTTP library that can be used in browsers and Node.js. Making the address

I’ve been reading some of the core axios source code, but recently I’ve been reading more about the axios build process and the implementation of important features. After all, you have to know what you’re doing. The common uses and related features of AXIos are not listed here, so if you are not sure, you can go here first. Space is limited, node environment related learning, you can further learn.

Source directory

The source code we are analyzing is in a large directory, /lib, as follows.

  • Adaapters adapter (for browser and Node environments)
  • Cancel Cancel request class (defines the class to request cancellation)
  • Core Function modules (core logic for request definition and implementation)
  • Helper (Auxiliary function module)
  • Axiox.js (axios export file, entry file)
  • Default.js (the default configuration file for Axios)
  • Utils (common utility class methods)

Axios workflow flowchart

Start with one map and make up the rest. (Browser environment)

How Axios works

Instead of focusing on some of axios’s extensions, let’s see how Axios works. Let’s go to the entry file axios.js

var bind = require('./helpers/bind'); // Bind context functions
var Axios = require('./core/Axios'); // Core code entry
var mergeConfig = require('./core/mergeConfig'); // Merge objects
var defaults = require('./defaults'); // Default configuration file

function createInstance(defaultConfig) { // Create an axios object instance
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);
  utils.extend(instance, Axios.prototype, context);
  utils.extend(instance, context);
  return instance;
}

// Create the default instance to be exported
var axios = createInstance(defaults);
modules.export.default axios
Copy the code
  1. We can see that the axios we’ve been using is returned through the createInstance method
  2. The return axios function in createInstance returns the context instance generated by the core class axios
  3. There are two key utility functions, extend and bind, that handle context instances
The extend functionfunction extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      // Assign the key value of b to Aa[key] = val; }});returna; } Bind is basically no different from native JS bind, which returns axios.prototype. requestfunction bind(fn, thisArg) {
  return function wrap() {
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    return fn.apply(thisArg, args);
  };
}
Copy the code

After this operation, the axios prototype’s methods and properties are already mounted on the properties of the Axios function returned by createInstance.

Core classes Axios

The Axios class is implemented in core/ axios.js,

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = { / / the interceptor
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Axios.prototype.request = function request(config) {
  // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1) | | {}; config.url =arguments[0];
  } else {
    config = config || {};
  }
  // Merge default and user-defined configurations
  config = mergeConfig(this.defaults, config);
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }
  // Define the promise chain
  var chain = [dispatchRequest, undefined];
  
  var promise = Promise.resolve(config);
  
  // Request interceptors are inserted forward, so the interceptor functions are executed in reverse order
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  // The corresponding interceptor is inserted backwards, so the order in which the interceptor functions are executed is continuous
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  // If and only if the chain is empty
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  // Finally return a promise
  return promise;
};
Copy the code

Let’s see what this Axios instantiation does with the request function.

  1. First, the default config and the custom config are merged.
  2. The promise chain array is then defined and a new Promise is generated through promise.resolve(config), which is then inserted into the chain array with the request interceptor and corresponding interceptor.
  3. Check the length of the chain array, and if length is not equal to 0, keep updating the promise value and return the final promise.

That’s why we can chain-call Axios, because we end up returning a Promise object. Then let’s analyze the important components one by one.

dispatchRequest

The code is in core/dispatchRequest.js, so we’re not going to focus on the cancellation logic, just the logic of sending the request

module.exports = function dispatchRequest(config) {
  // Determine whether the request is cancelled
  throwIfCancellationRequested(config);
  // Ensure headers exist
  config.headers = config.headers || {};
  // Transform request data
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );
  // Flatten headers
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers
  );
  utils.forEach(
    ['delete'.'get'.'head'.'post'.'put'.'patch'.'common'].function cleanHeaderConfig(method) {
      deleteconfig.headers[method]; });var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
    // Transform response data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );
    return response;
  }, function onAdapterRejection(reason) {
    if(! isCancel(reason)) { throwIfCancellationRequested(config);// Transform response data
      if(reason && reason.response) { reason.response.data = transformData( reason.response.data, reason.response.headers, config.transformResponse ); }}return Promise.reject(reason);
  });
};
Copy the code

The logic of the dispatchRequest function is very clear

  1. The data, headers, and transformRequest properties of config are processed using transformData. The flowchart summary is also verified. Confgi is transformData processed before XHR is sent
The transform functionvar utils = require('. /.. /utils');
module.exports = function transformData(data, headers, fns) {
  /*eslint no-param-reassign:0*/
  utils.forEach(fns, function transform(fn) {
    data = fn(data, headers);
  });

  return data;
};
Copy the code

As you can see from the code, the FNS passed in is actually an array of functions. Loop through the array of functions, pass the current data and headers attributes as parameters to each FN, and return the value as the value of the new data, so as to achieve multiple processing of config.data

// 'transformRequest' allows request data to be modified before being sent to the server
  // Only for 'PUT', 'POST', and 'PATCH' request methods
  // The function in the following array must return a string, either an ArrayBuffer, or a Stream
  transformRequest: [function (data, headers) {
    // Perform any conversion on data
    returndata; }].// 'transformResponse' allows the response data to be modified before being passed to then/catch
  transformResponse: [function (data) {
    // Perform any conversion on data
    returndata; }].Copy the code
  1. The config.headers field is deeply merged, and then the request method name traversal loop bound to headers is removed
  2. Config. adpater is used to determine whether the node or browser environment is used to call the HTTP module method of Node.js or the XHR method of browsing. In the default configuration file, core/default.js
function getDefaultAdapter() {
  var adapter;
  if (typeofXMLHttpRequest ! = ='undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeofprocess ! = ='undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}
Copy the code

Using the getDefaultadapter method to reference different files, the browser environment goes directly to

adapter = require('./adapters/xhr');
Copy the code
  1. Finally, transofromData will be processed for the response result returned by the request, similar to the principle of processing request parameters above.

xhr

As we said above, in the browser environment, the value of config.adapter eventually becomes

config.adapter = require('./adapters/xhr')
Copy the code

The XHR file also does some processing to the incoming config before sending the data, so we won’t analyze it. Here we will focus on the onreadyStatechange function of the XHR object. For convenience, the following code only intercepts the core logic

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
// Listen for ready state
    var xhr = new XMLHttpRequest()
    request.onreadystatechange = function handleLoad() {
      if(! request || request.readyState ! = =4) {
        return;
      }
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') = = =0)) {
        return;
      }

      // Prepare the response
      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
      varresponseData = ! config.responseType || config.responseType ==='text' ? request.responseText : request.response;
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };
      settle(resolve, reject, response);
      request = null; }; }}Copy the code

The xhrAdapter function returns a promise by creating an XHR object, listening for the onReadyStatechange event, and, on success, assembling the response variable and calling the settle method

settle(resolve, reject, response);
Copy the code

The definition is as follows:

module.exports = function settle(resolve, reject, response) {
  var validateStatus = response.config.validateStatus;
  if(! response.status || ! validateStatus || validateStatus(response.status)) { resolve(response); }else {
    reject(createError(
      'Request failed with status code ' + response.status,
      response.config,
      null, response.request, response )); }};Copy the code

This function is another encapsulation of resolve and reject. As long as Response. status is valid, response is directly used as the parameter resolve. The corresponding parameters are also mentioned in the official documentation

// 'validateStatus' defines a resolve or reject PROMISE for a given HTTP response status code. If 'validateStatus' returns' true' (or is set to 'null' or 'undefined'), promises will be resolved; Otherwise, the promise will be rejecte
  validateStatus: function (status) {
    return status >= 200 && status < 300; // default
  },
Copy the code

At this point, an Axios core process of building and sending requests is over. Next, the cancellation request and interceptor features mentioned above are added.

The interceptor

Interceptor class in the definition of the core/InterceptorManager js, refer to the above work flow chart.

'use strict';

var utils = require('. /.. /utils');

function InterceptorManager() {
  this.handlers = [];
}

InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null; }}; InterceptorManager.prototype.forEach =function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if(h ! = =null) { fn(h); }}); };module.exports = InterceptorManager;

Copy the code

The InterceptorManager is a function that holds the Handlers array.

  1. This is a big pity, which is fulfilled (like the promise. Then method). Put the objects containing the two functions into this. Handlers array, and return the index value of the current array
  2. Eject: Pass in the ID, which is actually the index of the Hanlders, and set the corresponding value to NULL through the index
  3. The forEach method iterates through the Handlers array and executes fn(handler) through the fn passed in externally.

Now that we’ve combed through the methods, let’s revisit the Axios initialization process,

function Axios(instanceConfig) {
  this.interceptors = {
    request: new InterceptorManager(), // Request interceptor
    response: new InterceptorManager() // Response interceptor
  };
}
Copy the code

We’re using Axios to define the interceptor that says,


// Add request interceptor
axios.interceptors.request.use(function (config) {
  // What to do before sending the request
  return config;
}, function (error) {
  // What to do about the request error
  return Promise.reject(error);
});
Copy the code

This is possible because the axios instance has the interceptors property attached to it and has interceptors for request and Response. In the following

Axios. Prototype. Request methodvar 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

As you can see, the Promise chain is defined in the request function to store the fulfilled and Rejected functions in the interceptor.

  1. As you can see from the code, the request interceptor function is placed before the dispatchRequest function, and the earlier the request interceptor is defined, it is placed after the chain array (because unshift of the array is performed).
  2. The corresponding interceptor is placed after the dispatchRequest function, and the earlier one is defined before the chain array (which executes the array’s push method).
  3. After the chain array is assembled, the chain array will be traversed. Each time, the first two functions will be passed in as the promise. Then the fulfilled and Rejected methods, and the promise value will be updated, so as to achieve the function of chain call.

And if you’re not too sensitive to this, you don’t know why you can do chain calls, but this is the same thing as, okay

let promise = Promise.resolve(config)
letnewPromise = promise.then(fn1, fn2).then(fn3, fn4).then(fn5, fn6) .then(.. ,..) .then(.. ,..)return newPromise
Copy the code

I believe that students who are familiar with Promise will understand the principle of chain call here, and those who are not familiar with it can also search the corresponding article on nuggets to learn. So when we request data using Axios,

axios(config).then(res= >...).Copy the code

RequestInterceptor ->transformRequest->dispatchRequest->transformResponse->responseInterceptorIf the responseInterceptor does not provide the response response, the response response is provided by the responseInterceptor. If the responseInterceptor does not provide the responseInterceptor response, the response response is provided by the responseInterceptor.

We have this logic in the dispatchrequst.js file

The adapter here is the promise returned by xhr.jsreturn adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
    // Transform response data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );
    return response
  }
Copy the code

Once the Adapter state becomes a pity, the onAdapterResolution function will be executed, and then the logic of transformData will be executed, and the response will be returned. Note that dispatchRequest.js itself returns Adapter (config).then(.. ,..) A new promise instance.

Maybe this is the big pity that we will fulfil the dispatchRequest function, then the config will be processed by the request interceptor. When we perform the following function: Promise = promise.then(dispatchRequest,...)Copy the code

Since dispatchRequest itself returns a promise, the promise becomes a promise returned by dispatchRequest, and then, if there is a response interceptor, it continues

promise = promise.then(responseFn1, ...)
Copy the code

ResponseFn1 is response, and the response interceptor is response, and we’ve converted from request to response.

Cancel the request

Cancel request this feature, I would like to call it the strongest, in the actual work also used a lot, often used in the scenario is mainly query list aspect, prevent data disorder.

Imagine such a scene, for example, you want to make a song list query list page, when you enter a keyword, will send a request, of course, this is very rude, front-end we will do anti-shake measures, reduce the cost of request, generally can meet.

Assume that if our time is set to 300 ms, but we search by keywords, the size of the data returned is different also, each request of network status may also have a lot of ups and downs, assuming that the first time we have after T1 (300 ms) launched the first request, but if this time the query data volume is very big and the network status is not very good, Assume that the response time is 700ms, and then we conduct a query at T2(300ms) after the request is initiated. The data volume of the query is small and the network status is good. Assume that the response time is 300ms.

You can see it clearly

T2 + time2 < time1
Copy the code

Shows for the first time, we will request response after the second response, this time will appear before a request data will overwrite the second request data, this creates disorder of data, in order to avoid the happening of this kind of situation, when we send the same request again, hope to send a new request, the request to stop off, did not respond to last time Axios also provides this feature for us.

Cancel and CancelToken class

Let’s start by looking at some examples of cancellation requests on the website

The first is to use the CancelToken attribute of Axios, and then generate a source object using the source method of that attribute. Each request is sent with source.token as the value of the CancelToken attribute of config. Cancel the request by calling source.cancel

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 {
     // Processing error}});// Cancel the request (the message argument is optional)
source.cancel('Operation canceled by the user.');
Copy the code

CancelToken: CancelToken: CancelToken: CancelToken: CancelToken: CancelToken: CancelToken: CancelToken: CancelToken: CancelToken: CancelToken: CancelToken

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // The executor function takes a cancel function as an argumentcancel = c; })});// cancel the request
cancel();
Copy the code

In cancer.js, find the cancel definition

function Cancel(message) {
  this.message = message;
}
Cancel.prototype.toString = function toString() {
  return 'Cancel' + (this.message ? ':' + this.message : ' ');
};
Cancel.prototype.__CANCEL__ = true;
module.exports = Cancel;
Copy the code

Find the code for cancelToken definition in cancel/ canceltoken.js

var Cancel = require('./Cancel');

function CancelToken(executor) {
  if (typeofexecutor ! = ='function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  // Assign a promise to this.promise in order to cancel the request asynchronously
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve; // Assign the resolve method from a promise to resolvePromise for later invocation
  });

  var token = this;
  // Execute the executor function with a function fn,
  // The argument to the fn function is an error message
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }
    // token.reason stores an instance of Cancel
    token.reason = new Cancel(message);
    // The call will change this. Promise from a pending state to a fulfilled state, asynchronously canceling the request
    resolvePromise(token.reason);
  });
}

CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason; }}; CancelToken.source =function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c; // C is a function that cancels the request
  });
  return {
    token: token,
    cancel: cancel
  };
};

module.exports = CancelToken;
Copy the code

Source CancelToken. Source is a further encapsulation of CancelToken. It is used in different ways but the principle is the same.

CacnelToken instantiation :(cancelToken takes an excutor function)

  1. Make a Promise from new Promise, then assign the Promise to this. Promise and the resolve method in the Promise method to a new variable, resolvePromise. This means that the resolvePromise will be able to switch the this.promise state in the future, thus implementing some asynchronous processing logic to the outside world.
  2. Execute the excutor function and pass the fn function as an argument to excutor.
  3. The input parameter of the fn function is an error message variable, and the following logic is executed internally
    • Token. reason exists and returns directly
    • Token. reason = new Cancel(message) resolvePromise(token.reason) resolve(token.reason) Change the state of this. Promise from pending to depressing

This. Promise will gradually change its state from pending to depressing. This is a big pity. Next, go to the Adapters /xhr.js file and find the code to cancel the adapters

if (config.cancelToken) {
    // Handle cancellation
   CancelToken is an instance of cancelToken.
   / / config. CancelToken. Promise state switch from pengding into fulfilled after operation is executed to cancel the request
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if(! request) {return;
    }

    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}
Copy the code

As you can see, if we define the cancelToken attribute on config, we will execute the then method of the Config. cancelToken promise attribute after the cancel function executes. Once the promise state is successfully switched, XHR’s ABORT method is then called to cancel a request that has not yet been responded to. Note that abort only cancellations requests that have not been responded to. The abort method does not have any effect if the request is executed.

conclusion

  1. Axios is a Promise based Http library that uses XHR in the browser environment and Http modules to send requests in the Node environment
  2. The principle of interceptor is to join the request interceptor before the request through the chain Promise chain, and join the response interceptor after the request, and loop through the chain to achieve the function of chain invocation. Before and after dispatchRequest, the parameters passed are changed from Request to response.
  3. The cancellation request is an asynchronous separation of the design scheme, using the asynchronous effect of promise, by switching the state of promise, so as to achieve the realization of asynchronous cancellation request.

If there are any mistakes in the article, I hope to point them out and I will correct them in time. Have a nice weekend!