background

In daily development, we often deal with interfaces, but in the development of modern standard front-end framework (Vue/React), we cannot do without Axios. Out of curiosity, I read the source code.

Unavoidably boring to read source code, easy to context relationship depend on each other to get a head of dew, we can seize the main contradiction, ignore the secondary contradictions, can combine the debugger to debug mode, the main process first sorted out, slowly chew details is better, the following is the source and the design idea behind, deficiency, please correct me a lot.

What is axios

  1. Promise wrapped HTTP request library (avoid callback hell)

  2. Supports browser side and Node side

  3. Rich configuration items: data converters, interceptors, etc

  4. The client supports XSRF defense

  5. Vue/React, peripheral plugins, etc.

Two other pieces of data demonstrate how widely Axios is used

1. As of the end of June 2021, Github’s star count was 85.4K

2. NPM’s weekly downloads reach tens of millions

Basic use of Axios

Source directory structure

Take a look at the catalog instructions, as follows

Execute the process

First look at the overall execution process, there is a general concept, the overall process will be detailed in the following points:

  1. axios.createCreate a separate instance, or use it directlyaxiosInstance (axios/axios.get...)
  2. requestThe method is the entrance,axios/axios.getAnd so on call will walk inrequestFor processing
  3. Request interceptor
  4. Request data converter for the passed parameterdataheaderDo some data processing, likeJSON.stringify(data)
  5. Adapter, determine whether it is browser side ornodeEnd, perform different methods
  6. Response data converter, which processes server-side data, for exampleJSON.parse(data)
  7. Responder interceptors that do server-side data processing, such astokenFailed to log out, error reporteddialogprompt
  8. Return data to the developer

Entry file (lib/axios.js)

As you can see from the following code, the exported AXIos is the instantiated object on which the create method is mounted to create a separate instance, thus isolating the instances from each other.

.// The method to create the instance procedure
function createInstance(defaultConfig) {
  return instance;
}
/ / instantiate
var axios = createInstance(defaults);

// Create a separate instance to isolate the scope
axios.create = function create(instanceConfig) {
  returncreateInstance(mergeConfig(axios.defaults, instanceConfig)); }; .// Export the instance
module.exports = axios;
Copy the code

If you’re curious about the createInstance method, let’s take a look.

function createInstance(defaultConfig) {
  // instantiate to create a context
  var context = new Axios(defaultConfig);

  // Get/POST is a call to request
  // Point this of the request method to context to form a new instance
  var instance = bind(Axios.prototype.request, context);

  // axios.prototype method (get/post...) Mount to a new instance instance,
  // And refer this to context in the prototype method
  utils.extend(instance, Axios.prototype, context);

  // The Axios property value is mounted to the new instance instance
  / / development to use axios. Default/interceptors
  utils.extend(instance, context);

  return instance;
}
Copy the code

As you can see from the above code, Axios does not simply create instance context, but does a series of context bindings and property methods to support Axios (), axios.get(), and so on;

The createInstance function is a core entry point, so let’s go through the process:

  1. By constructorAxiosCreate an instancecontextAs the followingrequestMethod context (thisPointing)
  2. willAxios.prototype.requestMethod is used as an example, and thethisPoint to thecontextTo form a new instanceinstance
  3. Constructor functionAxios.prototypeTo mount the new instanceinstanceAnd then put the prototype in each methodthisPoint to thecontext, can be used in developmentaxios.get/post..., etc.
  4. Constructor functionAxiosTo the new instanceinstanceOn, we can only use the following properties during development

axios.default.baseUrl = ‘https://… ‘ axios.interceptors.request.use(resolve,reject)

Request () : return new Axios(); request () : return Axios(); Just to support axios(), developers can write fewer lines of code…

Default configuration (lib/defaults.js)

From the createInstance method call, you discover that there is a default configuration, mainly built-in properties and methods, to override

var defaults = {
  ...
  // Request timeout period, not timeout by default
  timeout: 0.// Request a data converter
  transformRequest: [function transformRequest(data, headers) {... }].// Response data converter
  transformResponse: [function transformResponse(data) {... }],... }; .module.exports = defaults;
Copy the code

Constructor Axios(lib/core/ axios.js)

There are two main points:

  1. Configuration: External incoming, can override the internal default configuration
  2. Interceptor: After instance, the developer can passuseMethod to register successful and failed hook functions, such asaxios.interceptors.request.use((config)=>config,(error)=>error);
function Axios(instanceConfig) {
  / / configuration
  this.defaults = instanceConfig;
  // Interceptor instance
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
Copy the code

See what the prototype method Request does

  1. Supports multiple types of parameter transmission
  2. Configure the priority definition
  3. throughpromiseChain calls, executed sequentially
/ / pseudo code
Axios.prototype.request = function request(config) {
  // To support request(url, {... }), request({url, ... })
  if (typeof config === 'string') {
    config = arguments[1) | | {}; config.url =arguments[0];
  } else {
    config = config || {};
  }
  // Configure priority: call method configuration > Instantiate AXIOS configuration > Default configuration
  Get (url, {}) > axios.create(url, {}) > internal default Settings
  config = mergeConfig(this.defaults, config);
  // Interceptor (request and response)
  var requestInterceptorChain = [{
    fulfilled: interceptor.request.fulfilled,
    rejected: interceptor.request.rejected
  }];
  var responseInterceptorChain = [{
    fulfilled: interceptor.response.fulfilled,
    rejected: interceptor.response.rejected
  }];
  var promise;
  // Form an array of promise chains
  var chain = [].concat(requestInterceptorChain, chain, responseInterceptorChain);
  // Pass in configuration
  promise = Promise.resolve(config);
  // Form a promise chain call
  while(chain.length) { promise = promise.then(chain.shift(), chain.shift()); }...return promise;
};
Copy the code

An asynchronous promise invocation chain is formed by traversing the array, which is axios’ clever use of promises, represented by a graph

Interceptors (lib/core/InterceptorManager. Js)

The promise invocation chain mentioned above involves an interceptor, which is simpler and mounts a property and three prototype methods

  • handlerDeposit:useThe registered callback function
  • Use: registers successful and failed callback functions
  • Eject: Deletes registered functions
  • forEach: iterates through the callback function. It is usually used internally, for example:promiseCall chain that method, loop through the callback function, stored inpromiseCall chain in an array
function InterceptorManager() {
  // store the use registered callback function
  this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  // Register successful and failed callback functions
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
    ...
  });
  return this.handlers.length - 1;
};
InterceptorManager.prototype.eject = function eject(id) {
  // Delete the registered function
  if (this.handlers[id]) {
    this.handlers[id] = null; }}; InterceptorManager.prototype.forEach =function forEach(fn) {
  // Loop through the callback function
  utils.forEach(this.handlers, function forEachHandler(h) {
    if(h ! = =null) { fn(h); }}); };Copy the code

dispatchRequest(lib/core/dispatchRequest.js)

The dispatchRequest method that the promise calls in the chain does the following:

  1. transformRequest:configIn thedataTo process, for examplepostThe request ofdataStringing(JSON. Stringify (data))
  2. adapter: adapter that contains the browser sidexhrnodethehttp
  3. transformResponse: Processes server response data, for exampleJSON.parse(data)

DispatchRequest local figure

module.exports = function dispatchRequest(config) {...// The transformRequest method is context-bound to config, and processes data and headers
  config.data = transformData.call(
    config, // Context, that is, this points to
    config.data, // Request the body argument
    config.headers, / / request header
    config.transformRequest // Convert the data method
  );
  Adapter is an adapter that contains XHR on the browser side and HTTP on the Node side
  // There is a built-in Adapter, and it can also be customized externally to initiate Ajax requests
  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    // transformResponse method, context binding config, data and headers processing
    response.data = transformData.call(
      config, // Context, that is, this points to
      response.data, // The server responds to the data
      response.headers, // Headers for the server response
      config.transformResponse // Convert the data method
    );
    return response;
  }, function onAdapterRejection(reason) {...return Promise.reject(reason);
  });
};

Copy the code

Data converter (lib/core/transformData. Js)

The above mentioned data converter, easier to understand, the source code is as follows

module.exports = function transformData(data, headers, fns) {
  var context = this || defaults;
  // FNS: an array containing one or more method converter methods
  utils.forEach(fns, function transform(fn) {
    // Bind the context and pass in data and headers parameters for processing
    data = fn.call(context, data, headers);
  });
  return data;
};
Copy the code

The FNS method is the (request or response) data converter method, which is the default configuration defined in the initial defaults file, and can also be customized externally.

Axios(lib/defaults.js)

var defaults = {
  ...
  transformRequest: [function transformRequest(data, headers) {
    / / external incoming specification correct headers, such as (accept | accept) = > the accept
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type'); .if (utils.isObject(data) || (headers && headers['Content-Type'= = ='application/json')) {
      // Post/PUT /patch Requests carry data. The content-type header needs to be set
      setContentTypeIfUnset(headers, 'application/json');
      // string
      return JSON.stringify(data);
    }
    returndata; }].transformResponse: [function transformResponse(data) {...try {
      // The string is parsed to JSON
      return JSON.parse(data);
    } catch (e) {
      ...
    }
    returndata; }}],Copy the code

Pipe (dest2) SRC. Pipe (dest1). Pipe (dest2)

Adapter (lib/defaults. Js)

Mainly contains two parts of the source code, that is, the browser side XHR and the NODE side HTTP request, by judging the environment, the implementation of different end OF the API.

function getDefaultAdapter() {
  var adapter;
  if (typeofXMLHttpRequest ! = ='undefined') {
    / / the browser
    adapter = require('./adapters/xhr');
  } else if (typeofprocess ! = ='undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // node
    adapter = require('./adapters/http');
  }
  return adapter;
}
Copy the code

The unified API is provided externally, but the underlying API is compatible with browser and Node, similar to SDK. The changes of the underlying API do not affect the upper-layer API, maintaining backward compatibility

Initiate a request (lib/ Adapters /xhr.js)

If you are interested in the node server, check the source code (lib/ Adapters/HTTP.js).

Simple version of the flow chart shows the general content:

Source code is relatively long, the use of pseudo-code to represent the key part

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {...// Initialize an XMLHttpRequest instance object
    var request = new XMLHttpRequest();
    // Concatenate the URL, for example, https://www.baidu,com + / API /test
    var fullPath = buildFullPath(config.baseURL, config.url);
    / / initialize a request, joining together the url, for example: https://www.baidu, com/API/test +? 10 & a = b = 20
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    // Timeout disconnects. Default: 0 never times out
    request.timeout = config.timeout;
    // When the readyState property changes, readyState = 4 indicates that the request is complete
    request.onreadystatechange = resolve;
    // Cancel the request to trigger the event
    request.onabort = reject;
    // The event is triggered by a network fault
    request.onerror = reject;
    // Timeout triggers the event
    request.ontimeout = reject;
    // Standard browsers (with window and document objects)
    if (utils.isStandardBrowserEnv()) {
      // For non-same-source requests, you need to set withCredentials = true to enable cookies
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;
      if(xsrfValue) { requestHeaders[config.xsrfHeaderName] = xsrfValue; }}// The request object carries headers to request
    if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
          // If data is undefined, content-Type is removed, that is, requests such as POST, PUT, and patch are not
          delete requestHeaders[key];
        } else{ request.setRequestHeader(key, val); }}); }CancelToken is passed in from outside
    if (config.cancelToken) {
      // Waiting for a promise response, the external cancellation request is executed
      config.cancelToken.promise.then(function onCanceled(cancel) { 
        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }
    // Send the request
    request.send(requestData);
  });
};
Copy the code

Request (lib/cancel/CancelToken. Js)

First take a look at axios Chinese document usage

var CancelToken = axios.CancelToken;
var 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 Only {cancelToken: token}, which must have some connection to cancel

Look at the source code

  1. CancelTokenmountsourceMethod is used to create its own instance and returns{token, cancel}
  2. tokenIs the constructorCancelTokenAn instance of thecancelMethod receives the constructorCancelTokenOne of the innercancelFunction to cancel the request
  3. In creating an instance, one step is to create inpengdingThe state of thepromise, and hangs on the instance method, externally through the parameterscancelTokenPass the instance intoaxiosInternal, internal callcancelToken.promise.thenWait state change
  4. When an external method is calledcancelCancel the request,penddingThe state becomesresolveThat is, cancel the request and throwreject(message)
function CancelToken(executor) {
  var resolvePromise;
  CancelToken: new cancelToken (...); } passed into axios inside, * internal call cancelToken. Promise. Then wait for state changes, when an external method is called the cancel request, * pendding state becomes the resolve, Cancel the request and throw reject(message) */
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });
  // Keep this pointing to, internally callable
  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      // Cancel the direct return
      return;
    }
    // Call cancel externally to cancel the request, cancel instantiates, save the message and add the canceled request flag
    // New Cancel(message) = {message, __CANCEL__ : true}
    token.reason = new Cancel(message);
    // The above promise changes from pedding to resolve and carries a message to then
    resolvePromise(token.reason);
  });
}
// Mount static methods
CancelToken.source = function source() {
  var cancel;
  /** * The CancelToken constructor is instantiated, taking the callback function as an argument, and the callback function * receives the function C inside CancelToken, stored in the variable cancel, which cancels the request */ by calling cancel
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

module.exports = CancelToken;
Copy the code

conclusion

The above analysis can be summarized as follows:

  1. In order to supportaxios()Written succinctly for internal userequestFunction as the new instance
  2. usepromsieChain call clever method to solve the sequential call problem
  3. The data converter method uses arrays to store data and supports multiple transmission and processing
  4. The adapter is compatible with browser-side andnodeEnd, external to provide unityapi
  5. Cancel the request block and hold it externallypenddingState, controlpromiseExecution timing of

reference

Making Axios source

Axios documentation notes

Parsing Axios source code step by step, from the basics to the basics

tip

For more posts, visit Github at Star

Github front-end good articles series