新年假期弯道超车:每天都在使用的Axios源码竟然这么简单?---发送请求篇(一)

开头语

年前最后几天啦~公司的同事们都开始陆陆续续休假,各位掘友们手头上的工作也没有那么紧张了把。大家是不是跟我一样还在坚守岗位呢?这个时候最适合进行弯道超车啦!!!赶紧来跟我一起学习axios源码吧!!!

在阅读本篇文章前请确保以下几点:

  1. 了解promise的使用方法

  2. 了解javascript中的原型对象和原型链相关知识

  3. 熟练掌握axios的基本使用方式

  4. 本篇文章基于axios@0.19.0 版本,你可以从以下链接中获取源码:

    git clone https://github.com/axios/axios/tree/v0.19.0

一、 Axios基本介绍

特点:

axios官网中我们可以了解到axios具备的几大特点:

  1. 支持node端和浏览器端
  2. 支持Promise
  3. 支持请求,响应拦截器等高级配置
  4. 支持取消请求
  5. 良好的社区支持

二、Axios使用

scss 复制代码
axios(config): 通用/最本质的发任意类型请求的方式
axios(url[, config]): 可以只指定url发get请求
axios.request(config): 等同于axios(config)
axios.get(url[, config]): 发get请求
axios.delete(url[, config]): 发delete请求
axios.post(url[, data, config]): 发post请求
axios.put(url[, data, config]): 发put请求

axios.defaults.xxx: 请求的默认全局配置
axios.interceptors.request.use(): 添加请求拦截器
axios.interceptors.response.use(): 添加响应拦截器

axios.create([config]): 创建一个新的axios(它没有下面的功能)

axios.Cancel(): 用于创建取消请求的错误对象
axios.CancelToken(): 用于创建取消请求的token对象
axios.isCancel(): 是否是一个取消请求的错误
axios.all(promises): 用于批量执行多个异步请求
axios.spread(): 用来指定接收所有成功数据的回调函数的方法

三、源码结构

bash 复制代码
├── /dist/                     # 项目输出目录
├── /lib/                      # 项目源码目录
│ ├── /adapters/               # 定义请求的适配器 xhr、http
│ │ ├── http.js                # 实现http适配器(包装http包)
│ │ └── xhr.js                 # 实现xhr适配器(包装xhr对象)
│ ├── /cancel/                 # 定义取消功能
│ ├── /core/                   # 一些核心功能
│ │ ├── Axios.js               # axios的核心主类
│ │ ├── dispatchRequest.js     # 用来调用http请求适配器方法发送请求的函数
│ │ ├── InterceptorManager.js  # 拦截器的管理器
│ │ └── settle.js              # 根据http响应状态,改变Promise的状态
│ ├── /helpers/                # 一些辅助方法
│ ├── axios.js                 # 对外暴露接口
│ ├── defaults.js              # axios的默认配置 
│ └── utils.js                 # 公用工具
├── package.json               # 项目信息
├── index.d.ts                 # 配置TypeScript的声明文件
└── index.js                   # 入口文件

四、阅读源码之前先提出几个问题

axios 相信大家在项目里已经是用得非常熟练了,区别于其他文章,如果一上来就开始通篇分析源码,肯定会让一部分同学确实有种"头晕脑胀"的感觉,所以我觉得如果带着一些问题去阅读源码会让整个学习过程不那么枯燥且有趣一些。

以下列出了几条问题,同学们可以先看看自己掌握多少,如果感觉回答起来比较困难的话也没关系,接下来我们在阅读源码的过程中会一一解开问题的答案。

  1. ❓ 为什么axios可以支持浏览器环境node环境

  2. ❓ 既可以通过 axios(config)axios(url[,config])使用axios,又可以通过axios.request(config)或者 axios.get(url[, config])使用,那么axios到底是一个对象还是函数呢?

  3. axios.request(config)axios(config)请求有什么区别?

  4. axios.defaults.xxx 是如何生效的?

  5. ❓ 如果多次使用请求拦截器和响应拦截器会是什么效果?后面的拦截器配置会覆盖前面的拦截器配置吗?如果不会覆盖,那拦截器的执行顺序是什么?

    php 复制代码
        axios.interceptors.request.use(xxx)  // 第一次使用请求拦截器
        axios.interceptors.request.use(yyy)  // 第二次使用请求拦截器
        axios.interceptors.response.use(zzz) // 第一次使用响应拦截器
        axios.interceptors.response.use(www) // 第二次使用响应拦截器
  6. axios 的取消请求功能是如何实现的?

  7. axios.create([config]) 返回的实例和axios本身有什么区别?

五、入口文件: /axios/lib/axios.js

/axios/lib/axios.js 就是axios对外暴露的入口,我们从这个文件开始分析:

  1. 在第 64module.exports.default = axios;导出的是axios 这个变量,也就是我们用户平时默认使用的axios ,这个axios38 行中通过createInstance方法创造出来的。

  2. 可以看出我写的注释里面已经"剧透"了我们第三个问题的答案 (被导出给用户使用的axios其实本质是一个函数)

  3. 但是为什么axios还可以通过对象的方式来调用呢?全部玄机都在我们的createInstance里面。

1.createInstance()

javascript 复制代码
function createInstance(defaultConfig) {
  /* 
  创建Axios的实例
      原型对象上有一些用来发请求的方法: get()/post()/put()/delete()/request()
      自身上有2个重要属性: defaults/interceptors
  */  
  var context = new Axios(defaultConfig);

  // axios和axios.create()对应的就是request函数
  // Axios.prototype.request.bind(context)
  var instance = bind(Axios.prototype.request, context); // axios

  // 将Axios原型对象上的方法拷贝到instance上: request()/get()/post()/put()/delete()
  utils.extend(instance, Axios.prototype, context); 

  // 将Axios实例对象上的属性拷贝到instance上: defaults和interceptors属性
  utils.extend(instance, context);

  return instance;
}
  • 第一步:createInstance 方法接收的是一个defaultConfig 参数,此时defaultConfig 这个实参的值为defaults。 defaults 变量在同层目录下 default.js 文件中,这个变量是一个对象,对象具体有哪些属性我们先不看,得到这个 defaults变量后会传入Axios构造函数中,并且通过new 关键字生成一个context实例对象。

  • 第二步: 打开/axios/lib/core/Axios.js文件,我们看到Axios 构造函数 拿到了实例配置信息也就是instanceConfig 后会赋值给实例对象(也就是context变量 )一个defaults 属性和一个 Interceptors 属性,这是拦截器的相关信息,我们也暂时不看,后面会详细介绍。

  • 除此之外,Axios在自己的原型对象上添加了一个request函数 ,一个getUri函数 。还有通过工具方法增加了七个方法:'delete', 'get', 'head', 'options','post', 'put', 'patch'。这七个方法的功能是通过传入不同的config配置信息再调用request方法进行请求,这样能语义化更方便用户使用。

我们先看看现在生成的context实例对象长什么样子:

context变量

instance变量

再到下一步声明了一个instance 对象,这个bind方法是axios自己封装的工具方法,功能跟我们平常使用过的Function.prototype.bind方法没有太大的区别,如果你对bind方法和this指向不不熟悉,可以看看我的这两篇文章:

  1. 可能是你看过最完整的this指向总结!

  2. 改变This指向的三种方式及实现---call、apply、bind

instance本质上和request方法是没有区别的,只需要记住他是一个方法。

  • 第三步: 将Axios原型对象上的方法拷贝到instance上,那么现在的instance方法上就有了很多函数,如下图所示:
  • 第四步:再把Axios实例对象上的属性拷贝到instance 上,也就是增加了defaultsinterceptor属性,如下图所示:

最后返回instance ,这个instance就是平时我们默认使用的axios。

我们观察上面发现,无论是哪种使用方式,其实都是调用了 request 方法,真正发出请求的逻辑就在request 方法里面,接下来我们重点研究request方法

六、送请求文件: /axios/lib/core/Axios.js

1. Axios.prototype.request()

我们打开/axios/lib/core/Axios.js 文件,为了更好的学习,我们先去掉与拦截器处理相关的代码。只看最核心的逻辑代码,这样一来request的逻辑就非常简洁和易于理解了。

ini 复制代码
  
Axios.prototype.request = function request(config) {

  //Step1: 适配 Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  //Step2: 合并配置
  config = mergeConfig(this.defaults, config);
  // 添加method配置, 默认为get
  config.method = config.method ? config.method.toLowerCase() : 'get';

  //Step3: 使用promise链式调用的特点,将chain数组内的方法一个个取出来当成then方法的回调函数
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);
  
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

Step1: 适配 axios('get',{params:"xxx"}) 使用方式

axios会对传入的参数进行判断,如果第一个参数是字符串,那么axios会将这一个字符串当作请求方法的类型设置为config对象的url 属性,这也是axios提供给用户的第三种使用方式:axios('get',{params:"xxx"})

Step2: 合并config对象配置

对传入的config对象进行配置合并,如果没有用户没有传入method字段,则使用默认get请求方式

Step3: 请求逻辑

  1. 首先是声明了一个chain 数组,第一个参数是dispactchRequest 方法,第二个参数是undefinedaxios为什么要多此一举加入一个undefined呢,这其中的缘由我们在后面讲解拦截器时会解释,这恰恰是axios设计者在设计上思想非常巧妙的一点。

  2. 然后声明了一个成功状态的Promise类型赋值给promise 变量,继续往下看,此时chain 数组的长度为2 ,那么就会执行while循环,axios通过数组的shift() 方法,从数组头部开始取出数组第一个元素(dispatchRequest() )当成成功的回调函数,第二个元素(undefined)当成失败的回调函数。

  3. promise本身就是成功状态,所以promise只会执行成功的回调函数,不会执行后面的失败回调函数,那么哪怕失败函数是undefined也是没关系的。

  4. 那么在执行完then方法后,then函数的返回值是什么? then函数的返回值是一个新的Promise的实例对象,这是Promise能够实现链式调用的关键。但是这个新的Promise实例对象的值是什么呢? 是fullfilled状态?pending状态?还是rejected状态?

  5. 在此我们不去花费时间讲解promise的相关知识,我把四种情况列出来:

结论:

1、通过return 返回一个非promise的值,则新promise的状态fulfilled,值为return 的值

2、不做任何处理(不return == return undefined),所以根据结论1新promise的状态为fulfiled,值为undefined

3、通过throw主动抛出错误或者代码出现错误,则promise的状态为rejected,值为throw的值

4、通过return 返回一个promise对象,则新promise就是return的promsie

那么在当前的例子中,我们想知道是哪种情况,就得看看dispatchRequest方法里面的逻辑。

(请求最核心的逻辑真得来了,不要着急,我保证~)

七、请求核心文件:axios/lib/core/dispatchRequest.js

同样的,我们删减掉非必要代码,只保留最核心的处理逻辑

scss 复制代码
// axios/lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {

  /* 
  合并config中的baseURL和url
  */
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
  }

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

  /* 
  对config中的data进行必要的转换处理
  设置相应的Content-Type请求头
  */
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  /* 
  整合config中所有的header
  */
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

  //Step1: 获取config对象中的指定适配器,如果没有,就用默认适配器
  var adapter = config.adapter || defaults.adapter;

  //Step2:adapter是一个Promise实例,真正请求的底层逻辑在adapter函数里面
  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    /* 
    对response中还没有解析的data数据进行解析
    json字符串解析为js对象/数组
    */
    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);
  });
};

1:指定请求适配器

如果用户传入的config对象没有adapter 属性,则会去获取默认的适配器,那么默认的适配器是什么呢?答案就在/axios/lib/defaults.js文件中的getDefaultAdapter()方法里面。

javascript 复制代码
// /axios/lib/defaults.js

function getDefaultAdapter() {
  var adapter;
  // Only Node.JS has a process variable that is of [[Class]] process
  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  } else if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  }
  return adapter;
}

从注释中我们可以知道,如果当前环境中有process变量,那就是node.js环境,即使用node.js中的http来进行请求。 如果当前环境中有XMLHttpRequest,那就使用xhr来进行请求。是不是非常简单?那么我们在文章开头提出的第一个问题也就有了答案,原来axios是这样对不同环境进行适配。

2:http请求与XMLHttpRequest请求

至于/axios/lib/adapters/http.js/axios/lib/adapters/xhr.js两个文件我们不继续深究,只需要知道两种请求方式返回的都是Promise 实例对象,如果请求成功,这个Promise实例对象便是fullfilled状态。如果请求失败,便是reject状态。

javascript 复制代码
//axios/lib/adapters/http.js

module.exports = function httpAdapter(config) {
  return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
  ...
  });
}

//axios/lib/adapters/xhr.js
module.exports = function xhrAdapter(config) {
  // 返回一个promise
  return new Promise(function dispatchXhrRequest(resolve, reject) {
  ...
  });
}

八、回到 /axios/lib/core/Axios.js文件

我们经过剥丝抽茧终于知道了axios最底层发送请求的逻辑,现在进行思路'回溯',重新理一下思路:

axios(config) --> Axios.prototype.request(config) --> dispatchRequest() --> adapter()

  1. adapter方法返回的是一个Promise 实例,状态由请求是否成功决定
  2. dispatchRequest得到了adapter返回的Promise 实例,自身的状态由adapter返回的promise实例状态决定,并且返回Promise实例给request方法
  3. request得到了dispatchRequest返回的Promise 实例,自身的状态由dispatchRequest返回的promise实例状态决定,并且返回一个Promise实例给用户

最后小结

好了,真是一场酣畅淋漓的"较量啊"。到现在我们算是已经知道了axios发送请的来龙去脉并且还顺手解决了文章开头提出的几个问题,下一篇: 我们将继续探究axios拦截器的实现原理:新年假期弯道超车:每天都在使用的Axios源码竟然这么简单?---请求响应拦截器篇(二)

  1. ❓ 为什么axios可以支持浏览器环境node环境
  • 👉:axios可以根据当前环境不同来选择不同的请求适配器,如果当前环境有 process 变量则选择 node.jshttp来请求,如果当前环境有XMLHttpRequest 变量则 使用 XMLHttpRequest来请求
  1. ❓ 既可以通过 axios(config)axios(url[,config])使用axios,又可以通过axios.request(config)或者 axios.get(url[, config])使用,那么axios到底是一个对象还是函数呢?
  • 👉:axios本质是一个函数,他是通过bind函数与request方法对应:var instance = bind(Axios.prototype.request, context); 他的功能与request函数一样,所以可以通过axios(config)来请求。

  • 👉: axios 通过工具函数extend还获得了context(Axios实例对象)的属性 : utils.extend(instance, Axios.prototype, context), 所以可以通过 axios.get(url[, config])来请求。

  1. axios.request(config)axios(config)请求有什么区别?
  • 👉:功能上一样,axios函数本身就是通过bind函数与request对应,只是在使用语法上不同
  1. axios.defaults.xxx 是如何生效的?
  • 👉:通过utils.extend(instance, context);,axios获得了Axios实例对象上的属性:defaults和interceptor属性。
相关推荐
y先森2 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy2 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189112 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿3 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡4 小时前
commitlint校验git提交信息
前端
虾球xz4 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇4 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒4 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员5 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐5 小时前
前端图像处理(一)
前端