Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

axios源码分析 #17

Open
mhfe123 opened this issue Dec 16, 2018 · 0 comments
Open

axios源码分析 #17

mhfe123 opened this issue Dec 16, 2018 · 0 comments

Comments

@mhfe123
Copy link
Contributor

mhfe123 commented Dec 16, 2018

项目主要技术栈是Vue,所以我们在请求后端接口的时候一般会用到axios的组件,那么我们就来分析一下它的源码及它的实现。

axios主要目录结构

├── /dist/                     # 项目输出目录
├── /lib/                      # 项目源码目录
 ├── /cancel/                 # 定义取消功能
 ├── /core/                   # 一些核心功能
  ├── Axios.js               # axios的核心主类---------------------------这是其最核心部分
  ├── dispatchRequest.js     # 用来调用http请求适配器方法发送请求         |
  ├── InterceptorManager.js  # 拦截器构造函数                            |
  └── settle.js              # 根据http响应状态,改变Promise的状态--------
 ├── /helpers/                # 一些辅助方法
 ├── /adapters/               # 定义请求的适配器 xhr、http----这个文件夹封装了ajax的请求
  ├── http.js                # 实现http适配器
  └── xhr.js                 # 实现xhr适配器
 ├── axios.js                 # 对外暴露接口
 ├── defaults.js              # 默认配置 
 └── utils.js                 # 公用工具
├── package.json               # 项目信息
├── index.d.ts                 # 配置TypeScript的声明文件
└── index.js                   # 入口文件

由使用到探索

我们一般使用的时候都会先new一个axios实例出来,那么这个实例是怎么来的呢?里边都有什么呢?我们来看一下源代码最外层一个axios.js文件

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

通过源代码我们可以看到axios导出的axios对象中有一个create方法,还有个Axios对象,同时axios本身还是createInstance函数,并且他们都会传递一个参数进去,所以我们在使用axios的时候会有多种不同的调用方法,他们最终实例化的其实是core文件下的Axios.js中所写的Axios构造函数。

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

Axios.prototype.request = function request(config) {
  // ...省略代码
};

// 为支持的请求方法提供别名
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

上边是部分源代码,我们可以看到在这个构造函数中他会有个default对象,是用来存放我们的配置信息的,还有个interceptors对象,里边包了两个实例,分别对应了request拦截器和response拦截器。并且在Axios的原型链上有写了一个request方法,并对支持的请求方法都赋值了一个别名。

我们的配置config是如何生效的

我们看源代码会发现几乎每个方法都会有一个config参数,我们使用的时候的config就是通过这种方式贯穿整个axios的。我们来看一下我们使用的3种方式:

import axios from 'axios'

// 第1种:直接修改Axios实例上defaults属性,主要用来设置通用配置
axios.defaults[configName] = value;

// 第2种:发起请求时最终会调用Axios.prototype.request方法,然后传入配置项,主要用来设置“个例”配置
axios({
  url,
  method,
  headers,
});

// 第3种:新建一个Axios实例,传入配置项,此处设置的是通用配置
let newAxiosInstance = axios.create({
  [configName]: value,
});

对比我们刚才所说的axios实例的生成,我们可以看到这三种方式其实都会把config用过参数的形式传递进去,并且在每一步调用的时候生效。

请求拦截器和响应拦截器

我们在上边看axios的构造函数的时候看到他绑定了一个request拦截器和response拦截器,那么他们是做什么的呢?又是如何生效的呢?顾名思义,拦截器就是拦截请求的,可以在发送request之前或者接收response之前进行拦截,然后做一些事情达到我们的目的。

// /lib/core/InterceptorManager.js

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;
  }
};

// 遍历this.handlers,并将this.handlers里的每一项作为参数传给fn执行
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};
// /lib/core/Axios.js
Axios.prototype.request = function request(config) {
  // ...
  var chain = [dispatchRequest, undefined];

  // 初始化一个promise对象,状态微resolved,接收到的参数微config对象
  var promise = Promise.resolve(config);

  // 注意:interceptor.fulfilled 或 interceptor.rejected 是可能为undefined
  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);
  });

  // 添加了拦截器后的chain数组大概会是这样的:
  // [
  //   requestFulfilledFn, requestRejectedFn, ..., 
  //   dispatchRequest, undefined,
  //   responseFulfilledFn, responseRejectedFn, ....,
  // ]

  // 只要chain数组长度不为0,就一直执行while循环
  while (chain.length) {
    // 数组的 shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
    // 每次执行while循环,从chain数组里按序取出两项,并分别作为promise.then方法的第一个和第二个参数

    // 按照我们使用InterceptorManager.prototype.use添加拦截器的规则,正好每次添加的就是我们通过InterceptorManager.prototype.use方法添加的成功和失败回调

    // 通过InterceptorManager.prototype.use往拦截器数组里添加拦截器时使用的数组的push方法,
    // 对于请求拦截器,从拦截器数组按序读到后是通过unshift方法往chain数组数里添加的,又通过shift方法从chain数组里取出的,所以得出结论:对于请求拦截器,先添加的拦截器会后执行
    // 对于响应拦截器,从拦截器数组按序读到后是通过push方法往chain数组里添加的,又通过shift方法从chain数组里取出的,所以得出结论:对于响应拦截器,添加的拦截器先执行

    // 第一个请求拦截器的fulfilled函数会接收到promise对象初始化时传入的config对象,而请求拦截器又规定用户写的fulfilled函数必须返回一个config对象,所以通过promise实现链式调用时,每个请求拦截器的fulfilled函数都会接收到一个config对象

    // 第一个响应拦截器的fulfilled函数会接受到dispatchRequest(也就是我们的请求方法)请求到的数据(也就是response对象),而响应拦截器又规定用户写的fulfilled函数必须返回一个response对象,所以通过promise实现链式调用时,每个响应拦截器的fulfilled函数都会接收到一个response对象

    // 任何一个拦截器的抛出的错误,都会被下一个拦截器的rejected函数收到,所以dispatchRequest抛出的错误才会被响应拦截器接收到。

    // 因为axios是通过promise实现的链式调用,所以我们可以在拦截器里进行异步操作,而拦截器的执行顺序还是会按照我们上面说的顺序执行,也就是 dispatchRequest 方法一定会等待所有的请求拦截器执行完后再开始执行,响应拦截器一定会等待 dispatchRequest 执行完后再开始执行。

    promise = promise.then(chain.shift(), chain.shift());

  }

  return promise;
};

dispatchrequest做了什么事情

我们再看拦截器生效的时候发现在Axios.prototype.request 中会有这么一段代码var chain = [dispatchRequest, undefined];,并且会在promise = promise.then(chain.shift(), chain.shift());被调用,那么这个dispatchrequest是什么呢?

// /lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // Support baseURL config
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
  }

  // Ensure headers exist
  config.headers = config.headers || {};

  // 对请求data进行转换
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // 对header进行合并处理
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

  // 删除header属性里无用的属性
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

  // http请求适配器会优先使用config上自定义的适配器,没有配置时才会使用默认的XHR或http适配器,不过大部分时候,axios提供的默认适配器是能够满足我们的
  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(/**/);
};

通过源码分析,我们可以看到dispatchrequest主要做了3件事情:

  • 拿到config对象,对config进行传给http请求适配器前的最后处理;

  • http请求适配器根据config配置,发起请求 ;

  • http请求适配器请求完成后,如果成功则根据header、data、和config.transformResponse拿到数据,

转换后的response,并return。

段落小结

通过上面对axios源码结构分析,我们可以得到axios搭建的一个主要结构:

调用===>Axios.prototype.request===>请求拦截器interceptors===>dispatchRequest===>请求转换器transformRequest===>请求适配器xhrAdapter===>响应转换器transformResponse===>响应拦截器interceptors

axios如何基于promise搭建异步桥梁

了解了axios的主要结构及调用顺序,那我们来看一下axios是如何通过promise来搭建异步的桥梁?

axios.get(/**/)
.then(data => {
  // 此处可以拿到向服务端请求回的数据
});
.catch(error => {
  // 此处可以拿到请求失败或取消或其他处理失败的错误对象
});

我们那get方法来举例,当我们调用axios的get方法的时候实际上是调用axios原型链上的get方法,而其原型链上的get方法又指向了其原型链上的request方法。

utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});
Axios.prototype.request = function request(config) {
  // ...
  var chain = [dispatchRequest, undefined];
  // 将config对象当作参数传给Primise.resolve方法
  var promise = Promise.resolve(config);

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

我们可以看到在request方法里边有个chain数组,这个其实是相当于一个方法的队列,这里会把request的拦截器插入chain数组的前边,response拦截器插入chain数组后边,通过下边的while循环,两两调用promise.then来顺序执行请求拦截器到dispatchrequest再到响应拦截器的方法。

在dispatchrequest中呢又会调用封装好的ajax(xhrAdapter方法),xhrAdapter方法返回的是还一个Promise对象

// /lib/adapters/xhr.js
function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // ... 省略代码
  });
};

xhrAdapter内的XHR发送请求成功后会执行这个Promise对象的resolve方法,并将请求的数据传出去,
反之则执行reject方法,并将错误信息作为参数传出去。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant