axios源码梳理

188 阅读13分钟

前言

最近在项目中使用axios封装Ajax请求时,看到调用axios请求写法有很多种,以前都是习惯使用axios({})这种,很少使用其他写法,感觉还挺有趣,干脆看看源码,整理整理

若文中有什么错误之处,欢迎指出和探讨

正文

环境准备

还是直接在Vue项目中封装调试了,方便也符合我的应用场景

vue-cli 起一个简单的项目,然后安装 axios 依赖,在 utils 目录下新建 request.js 文件,用来简单封装 axios

//utils/request.js
import axios from 'axios'
const service = axios.create({
  baseUrl: 'api',
  timeout: 10000
})
service.interceptors.request.use(
  config => {
    console.log('发出请求')
    return config
  },
  error => {
    console.log('请求失败')
  }
)
service.interceptors.response.use(
  response=>{
    console.log('响应成功:', response)
    return response
  }
)
export default service

 

写一个用户登录的测试接口

//api/user.js
import  axios from '@/utils/request'
export function login (data) {
  return axios({
    url: '/login',
    data: data,
    method: 'post'
  })
}

 

初始化

const service = axios.create( config )

 

axios定义在 lib/axios.js 文件中

'use strict';
var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
var defaults = require('./defaults');
/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 * @return {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);
  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);
  // Copy context to instance
  utils.extend(instance, context);
  return instance;
}
// Create the default instance to be exported
var axios = createInstance(defaults);
// Expose Axios class to allow class inheritance
axios.Axios = Axios;
// Factory for creating new instances
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
module.exports = axios;
// Allow use of default import syntax in TypeScript
module.exports.default = axios;

 

先不急着看代码,可以打印看一下模块导出的axios是什么

console.log(axios)
console.log(Object.keys(axios))

这里的 wrap 函数,应该是定义的一个通用工具类函数,它返回的是 fn 绑定 thisArg这个 上下文后执行的结果

fn,thisArg是什么后面调试看代码的时候再说,现在能明确的是使用 aixos 时,import 导入的,实际上是一个函数,同时这个函数(Object),有18个属性(JS中,可以给函数添加属性,添加的属性可通过 Object.keys() 方法获得

下面开始调试梳理流程

预处理(参数预处理)

从入口文件可以看出来,实例化 axios

调用 axios.create( config )方法

 

该方法使用工厂模式实例化 一个新的 axios,除了会接受一个 config,还会调用 mergeConfig() 方法添加一些默认属性

关于这个mergeConfig()   方法具体逻辑就不看了,它会将我们传入的config及 默认的一些属性一并处理返回作为实例化axios的config参数,

// Factory for creating new instances
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

可以打印看一下执行这个方法后的返回值

如果我们传入了用来添加至实例的参数,它会一并遍历处理添加至这个Object,作为实例化 axios 的参数

后面会进入 createInstance() 这个方法开始实例化,看一下这个方法是如何实例化 axios的

实例化

主要逻辑
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);
  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);
  // Copy context to instance
  utils.extend(instance, context);
  return instance;
}

 

createInstance 函数通过函数也可以看出来,是用来实例化 Axios 的,上面提到的module导出的 axios 就是由它实例化返回的。axios axios.create() 均会调用 createInstance 来实例化,二者的区别在于

axios是已经生成的一个默认配置的实例,而 axios.create() 接受参数用来自定义配置后生成一个新的实例

继续看这个 createInstance 函数

  1. 首先,new 了一个 Axios 实例 (context)
  2. 将Axios的 request 方法绑定一个执行上下文,这个上下文就是 刚才new的 Axios的一个实例
  3. Axios.prototype 上的可枚举属性遍历 深度复制到 instance 上,如果某个属性是函数 function ,绑定context 这个上下文到该 function 的执行作用域 
    代码中已经给出了注释,还是可以看一下这个extend 方法(明白大概流程就行,这些都是具函数,目的是了解这些方法做了什么,不必逐句分析)

    function forEach(obj, fn) {
      // Don't bother if no value provided
      if (obj === null || typeof obj === 'undefined') {
        return;
      }
      // Force an array if not already something iterable
      if (typeof obj !== 'object') {
        /*eslint no-param-reassign:0*/
        obj = [obj];
      }
      if (isArray(obj)) {
        // Iterate over array values
        for (var i = 0, l = obj.length; i < l; i++) {
          fn.call(null, obj[i], i, obj);
        }
      } else {
        // Iterate over object keys
        for (var key in obj) {
          if (Object.prototype.hasOwnProperty.call(obj, key)) {
            fn.call(null, obj[key], key, obj);
          }
        }
      }
    }
    function 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);
      };
    };
    function extend(a, b, thisArg) {
      forEach(b, function assignValue(val, key) {
        if (thisArg && typeof val === 'function') {
          a[key] = bind(val, thisArg);
        } else {
          a[key] = val;
        }
      });
      return a;
    }
    
  4. 将 context 这个实例上所有的可枚举属性深度复制到 instance 上,并且不绑定上下文(至于为什么这里还不太清楚,继续往下看吧)

createInstance 这个方法流程基本清楚了,现在只需要具体了解一下两个东西

  • context
  • instance,即 Axios.prototype.request 这个方法

搞清楚这两个具体内容,实例化  这个流程就基本跑完了

context

context,由代码就可以看出来,是Axios这个类的一个实例

var context = new Axios(defaultConfig);

 

 还是先打印看一下,这个实例是什么,有哪些属性,然后再去看这个 Axios 类

进入定义 Axios 这个类的代码,在 /core/Axios.js 文件中

//构造函数
function Axios(instanceConfig) {
  // instanceConfig 是一个对象,属性是用户自定义的和默认的属性
  this.defaults = instanceConfig;
  // interceptors 即我们常用的axios的拦截器对象
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

 

构造函数比较简单,做了以下两件事

  • 给实例的 defaults 属性初始化了一些属性,比如我们常见的 请求的 header,timeout等,上图 context 已经打印出来了,也可以看到。
  • 初始化拦截器,这里还没有添加我们自定义的拦截方法到拦截器
    为啥这里我要说到这一步还没有添加自定义的拦截方法到拦截器,因为我看到new InterceptorManager的时候没有传参,所以得出结论。后来一想,到这一步还在实例化 axios 阶段,也就是我们封装时的 axios.create() 阶段,压根还没到自定义拦截器方法( service.interceptors.request.use() service.interceptors.response.use() )的阶段,脑子抽了。所以应该是,此时拦截器还未自定义(不存在自定义的拦截器的拦截方法)

Axios.js 这个文件里其他部分的代码,都是在做一件事,就是给 Axios prototype 对象添加属性,代码的逻辑不细看了,后面用到再分析,还是先打印看一下添加了哪些属性,继续走实例化的流程

console.log('Axios.prototype',Object.keys(Axios.prototype))

回到上文的主线 axios.js 中的 createInstance 方法

context 已经明确是什么了

它是 Axios 类的一个实例defaults 属性是包含我们自定义的配置和默认配置的对象interceptors 是一个拦截器对象,有 request 拦截器 response 拦截器

至于下一步的 instance ,也就是上面分析  createInstance 函数大概流程的时候的 第 2, 3和 4 步

instance

它实际上就是  Axios.prototype.request 方法,只不过改变了该方法的执行上下文,也就是说,我们 import 到项目里的 axios,其实是 Axios.prototype.request 方法。初始化部分 开始就打印过我们导入的 axios 模块,留了两个疑问 fn 和 thisArg,现在已经明确了

上面 分析 Axios.js 文件的时候没看这个 方法,现在看一下这个方法的逻辑

因为这段代码在我们实例化后以  axios() 方式调用时才会执行,具体逻辑在后面调用部分再一步步分析,这里简单分析一下这段代码作用就行

有一个细节提一下

关于函数的 arguments,它是调用时传入的实参生成的数组,传入几个参数该数组的长度就是多少,与函数定义时定义的形参个数无关,写一段测试代码就明白了

var test = function (a) {
    console.log(arguments)
    console.log(a)
}
test('a',1,2)
//Arguments(3) ["a", 1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// a

  Axios.prototype.request 部分代码

/**
 * Dispatch a request
 *
 * @param {Object} config The config specific for this request (merged with this.defaults)
 */
Axios.prototype.request = function request(config) {
  // Allow for axios('example/url'[, config]) a la fetch API
  //接收一个形参 config(调用该方法时传入的第一参数)
  //判断传入的第一个参数的类型
  if (typeof config === 'string') {
    //传入的第二个参数赋值给 config
    config = arguments[1] || {};
    //添加url属性至 config,值为传入的第一个参数
    config.url = arguments[0];
  } else {
    config = config || {};
  }
  /*  因为绑定的执行上下文是 new Axios()生成的实例,this.defaults就是实例的defaults属性
      mergeConfig方法做的事情是将 config 和 defaults属性处理合并
  */
  config = mergeConfig(this.defaults, config);
  // 设置 config的 method属性
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }
  // 连接拦截器中间件
  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;
};

 

这段代码,大致流程是根据我们传入的配置和自定义的配置,生成了一个http请求,并添加了拦截器中间件,所以我们调用 axios()会根据传入的参数发起一个http请求

所以,上面第 2 步,是将instance 定义为一个发起http请求的方法

第 3 步,很简单,instance 是 request 方法,通过extend 方法将Axios.prototype对象的可枚举属性添加至 instance(再说一下,function也是对象,也可以添加属性,上面已经写过测试代码),如果属性是function,还要给它绑定执行作用域

第 4 步,还是一样的流程,通过extend方法将 context对象的可枚举属性添加至instance,只不过少了一步,将类型是 function的属性的绑定执行上下文,这个疑问在上面已经抛出来了

实例化的流程就先到这里吧,流程很简单,废话有些多了

 

之所以花这么多时间走这个流程,或者想走一下axios源码流程的初衷,是因为一个错误的写法。当我试图使用 axios.interceptors 给请求添加拦截器属性,发现报了一个undefined错误,于是我打印axios,得到了一个函数,而这个函数只是一个工具类函数,而不是我们常见的写法,一个显示的函数、类或者Object。这种方式和思想之前是没有去详细了解过,所以更多的是了解学习这种思路。

实例化部分到这里吧,后续再写其他部分的源码梳理

 

调用

调用部分,就从 api 入手,分析几个常用的 api 的流程,整个 axios 的结构就基本清楚了

Requests

axios(config)
// Send a POST request
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
})

 

这一步上面已经分析过了,其实就是调用了 Axios.prototype.request 方法。现在重点看一下这个方法

Axios.prototype.request = function request(config) {
  // Allow for axios('example/url'[, config]) a la fetch API
  //接收一个形参 config(调用该方法时传入的第一参数)
  //判断传入的第一个参数的类型
  if (typeof config === 'string') {
    //传入的第二个参数赋值给 config
    config = arguments[1] || {};
    //添加url属性至 config,值为传入的第一个参数
    config.url = arguments[0];
  } else {
    config = config || {};
  }
  /*  因为绑定的执行上下文是 new Axios()生成的实例,this.defaults就是实例的defaults属性
      mergeConfig方法做的事情是将 config 和 defaults属性处理并合并
  */
  config = mergeConfig(this.defaults, config);
  // 设置 config的 method属性
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }
  // 连接拦截器中间件
  var chain = [dispatchRequest, undefined];
  //生成一个Promise对象
  var promise = Promise.resolve(config);
  /*
    将 request (请求前)拦截器 (两个方法)两两插入chain 数组的头部,
    插入的相邻的两项分别为 fulfilled 和 rejected 两种状态时调用的函数
  */
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  //将 response (请求后)拦截器推入到 chain 数组的尾部
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  /*
    Promise.then链式调用 chain 数组里的方法
    Promise.then 接受两个参数,第一个参数是resolved状态的回调函数,
    第二个参数(可选)是rejected状态的回调函数。
    shift()方法会删除数组第一个元素,并返回该元素
    每执行一次 Promise.then(chain.shift(), chain.shift()),
    chain 数组头部相邻的两个元素被删除,第一个作为 Promise状态变为 resolved 时调用的方法
    被传入,第二个作为 Promise状态变为 rejected 时调用的方法被传入
    所有 请求前拦截器 被 Promise.then 依次执行后执行 dispatchRequest(http请求方法)
    http请求以 Promise 方式执行完后开始 以同样的方式依次执行 请求后拦截器
   */
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
}

 

这个方法我在代码中已经给出了比较详细的注释,重点在三个地方

  • chain 数组
  • Promise.then() 方法依次调用 chain 数组中的方法
  • dispatchRequest 方法

前两个代码注释已经很清楚了,下面看一下 dispatchRequest 方法的源码

function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}
/**
 * Dispatch a request to the server using the configured adapter.
 *
 * @param {object} config The config that is to be used for the request
 * @returns {Promise} The Promise to be fulfilled
 */
module.exports = function dispatchRequest(config) {
  // 取消请求的方法
  throwIfCancellationRequested(config);
  // Ensure headers exist
  config.headers = config.headers || {};
  // 转换请求的数据
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );
  // 合并自定义的配置和默认配置
  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) {
      delete config.headers[method];
    }
  );
  var adapter = config.adapter || defaults.adapter;
  // adapter即为发起http请求的方法
  return adapter(config).then(function onAdapterResolution(response) {
    //取消请求
    throwIfCancellationRequested(config);
    // 转换请求后的数据
    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);
  });
};

 

这部分代码逻辑还是比较清晰的,重点在 取消请求adapter 方法

取消请求在后面使用时再去看源码

简单看一下 adapter 部分的代码

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    //   浏览器环境
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    //  node 环境
    adapter = require('./adapters/http');
  }
  return adapter;
}
var defaults = {
  adapter: getDefaultAdapter()
}

 

这个文件中的代码,做的是根据运行环境来创建请求。axios之所以既能在浏览器端运行,又能在node.js中运行,主要代码就在这里。

  • 浏览器端,通过 XMLHttpRequest 对象创建请求
  • node.js 中,通过 http 模块创建请求

创建请求的具体逻辑就不看了,了解 axios 发起请求逻辑就可以了

axios(url[, config])

这种写法的处理逻辑,其实前面已经看过了

Axios.prototype.request = function request(config) {
  // Allow for axios('example/url'[, config]) a la fetch API
  //接收一个形参 config(调用该方法时传入的第一参数)
  //判断传入的第一个参数的类型
  if (typeof config === 'string') {
    //传入的第二个参数赋值给 config
    config = arguments[1] || {};
    //添加url属性至 config,值为传入的第一个参数
    config.url = arguments[0];
  } else {
    config = config || {};
  }
// ......
}

判断传入的第一个参数类型,如果是 string,便会将该值作为请求的 url

Request method aliases
  • axios.request(config)
  • axios.get(url[, config])
  • axios.delete(url[, config])
  • axios.head(url[, config])
  • axios.options(url[, config])
  • axios.post(url[, data[, config]])
  • axios.put(url[, data[, config]])
  • axios.patch(url[, data[, config]])

第一种的  request方法,其实就是 axios(),上面已经清楚了

现在看其他几种写法的处理逻辑

代码还是在 Axios.js 文件中

// Provide aliases for supported request methods
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
})

 逻辑很简单,就不细说了,流程就是把这七种 http 请求方式 添加到原型上,相当于做了语义化处理,让我们以请求方式的形式调用,预处理后还是调用 request 方法