Axios源码解析(部分)

2023-05-16

从 Github 上把 Axios 项目的 master 分支拷贝到本地,用编辑器打开项目目录

首先我们先解析一下整个 Axios 项目的一些关键的文件结构

image_1
//对照项目的文件目录,梳理一下其中的一些关键文件夹以及它的作用
// axios源码总体结构

.
├── dist # 存放压缩过后的axios代码
├── examples # 存放axios方法应用示例
├── lib # 存放axios核心代码
├── sandbox # 搭建模拟服务器

// axios核心代码目录结构
.
├── lib
    |—— adapters # 适配器(用于包装http和xhr对象)
        |—— http.js # http适配
        |—— xhr.js # xhr对象适配
    |—— cancel # 取消操作实现(即用于取消发送的http请求,CancelToken)
    |—— core  # Axios对象构建以及其核心方法实现
        |—— Axios.js # Axios的构造函数文件
        |—— buildFullPath.js # 构建完整的URL路径函数
        |—— createError.js # 构建Error对象
        |—— dispatchRequest.js # 发送请求函数,axios就是调用此函数来发送http请求
        |—— enhanceError.js # 用来更新错误对象内部的配置数据, createError函数内部通过调用此函数最终生成指定信息的Error对象
        |—— InterceptorManager.js # Axios对象的拦截器相关方法
        |—— mergeConfig.js # 合并配置项内容,Axios对象中通过此方法来合并默认的请求头信息和传入的新的相关头信息
        |—— settle.js # 根据响应状态码改变Promise状态为resolve()成功状态或reject()失败状态
        |_ transformData.js # 格式化请求或响应的数据
    |—— env # 存放axios版本信息,文件内容会随着版本自动更新
    |—— helpers # 存放一些用于axios内部的通用方法以及模块
    |—— axios.js # 入口文件
    |——default.js # axios对象内部的默认值
    |——utile.js # 存放通用方法

接下来我们从入口文件 index.js 开始,梳理一下一个完整的 axios 创建的过程

// axios.js

// 其实通过createInstance函数内部我们可以看到,最终函数返回的instance其实就是Axios.prototype.request方法,并且通过封装的bind函数将其绑定到了new出来的Axios实例对象的上下文环境当中,并且由于axios对象本质上是一个方法,所以我们在使用的时候就可以通过axios({method: 'POST', url: 'xxxxxxxxx'})这样的函数传参方式来实现请求的发送,或者可以直接通过axios.get()这样存在于axios对象原型链上的方法直接发送请求。

function createInstance(defaultConfig) {
  // 创建一个新的Axios实例对象,defaultConfig为默认的配置项
  var context = new Axios(defaultConfig);
  // 通过bind方法将Axios.prototype.request方法绑定到新的Axios实例对象上
  var instance = bind(Axios.prototype.request, context);

  // 将绑定到context上的Axios原型上的方法和属性继承到instance上
  utils.extend(instance, Axios.prototype, context);

  // 将context上的方法和属性继承到instance上
  utils.extend(instance, context);

  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

// 创建默认的可以导出的axios对象,也就是我们在项目中所使用的axios对象
// default传参是默认设置的配置选项
var axios = createInstance(default)


通过 axios.js 入口文件,就可以创建出一个默认的可以使用的 axios 对象,这里顺便提一下位于同一层级的 default.js 和 utils.js 两个文件,default.js 文件的作用就是用于生成一个默认的可以引用的配置项,即我们在 axios.js 中向 createInstance()方法传入的 default 参数,而 utils.js 文件则是存放了一些用于处理数据的公共方法,如特定的 forEach()遍历方法以及 merge()合并参数的方法。

我们通过 axios.js 中的 createInstance(defaultConfig)可以了解到,之所以我们使用 axios()可以发送请求并且通过传参的方式来实现对于请求的配置,本质上就是通过 Axios 对象以及其原型链上的 Axios.prototype.request()方法来实现的,那么接下来,我们接着分析一下 Axios.js 这个实现请求发送的核心文件以及其中的方法

// Axios.js

// Axios方法本身内容很少,其内部的defaults属性用来存放初始传入的默认配置项,并且在其上添加拦截器方法
// 拦截器对象分为请求拦截器和响应拦截器,均可以通过axios.interceptors.request.use(fulfilled, rejected, options)和axios.interceptors.response.use(fulfilled, rejected, options)来添加多个拦截器,关于拦截器的文件在后面会单独梳理,在这里我们只需要了解到Axios方法内部目前存在一个默认的配置项属性和默认的拦截器方法,并且在axios.js中的createInstance()方法里面,通过bind()方法,将Axios内部的属性和方法成功的添加到了我们最终使用的axios对象上
function Axios(instanceConfig) {
  // 默认的配置项
  this.defaults = instanceConfig;
  // 默认的拦截器方法
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager(),
  };
}

// 在Axios的原型链上首先添加requset方法,也就是我们调用的axios()实现发送请求的本质,它的参数config也就是我们在使用axios({method: 'POST', url: 'xxxxx'})时传入的对象参数
Axios.prototype.request = function request(config) {
  // axios对象还可以允许传入字符串形式的参数('example/url'[, config])
  // 两种方式的处理,如果传入字符串,则通过arguments来对字符串进行分割,如果传入对象类型则直接赋值,不传则为空对象
  if (typeof config === "string") {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  // 合并默认的配置项和新传入的配置项
  config = mergeConfig(this.defaults, config);

  // 设置请求类型,传入则为传入值的小写,否则为默认的get方式
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = "get";
  }

  // 省略部分关于一些配置项的处理,由于其中很多是对默认传入的配置项和我们在使用时传入的参数之间的合并或者格式化,在解析里就不一一展示

  // 存放请求拦截器方法数组
  var requestInterceptorChain = [];
  var synchronousRequestInterceptors = true;

  // 遍历实例对象上的请求拦截器
  // 可以用this.interceptors.request.handles.forEach()来实现相同的效果
  // 这里使用的forEach()方法是作者在InterceptorManager.js文件中封装的专门用来遍历拦截器数组的方法,其存在于拦截器对象的原型链当中,这里先简单做一个介绍,后续会有专门针对拦截器文件的解析,再具体对其中的方法和属性进行代码展示
  this.interceptors.request.forEach(function unshiftRequestInterceptors(
    interceptor
  ) {
    if (
      typeof interceptor.runWhen === "function" &&
      interceptor.runWhen(config) === false
    ) {
      return;
    }

    synchronousRequestInterceptors =
      synchronousRequestInterceptors && interceptor.synchronous;

    // 将拦截器中的成功回调和失败回调压入请求拦截器数组中
    // 注意,请求拦截器是以栈的方式,所以遍历数组输出时顺序是后进后出,逆序
    requestInterceptorChain.unshift(
      interceptor.fulfilled,
      interceptor.rejected
    );
  });

  var responseInterceptorChain = [];

  // 遍历实例对象上的响应拦截器
  // 注意,响应拦截器是以队的方式,所以遍历数组输出时顺序是先进先出,顺序
  this.interceptors.response.forEach(function pushResponseInterceptors(
    interceptor
  ) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  var promise;

  // 当添加的拦截器没有返回值,则不会跳转到此处执行,拦截器有返回值或无拦截器的情况下都会跳转到此处执行
  if (!synchronousRequestInterceptors) {
    // 这个时候dispatchRequest方法的参数config是undefined
    var chain = [dispatchRequest, undefined];

    // 开始拼接最终输出的chain数组,将请求拦截器加入到数组头部,响应拦截器拼接到数组尾部
    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    chain = chain.concat(responseInterceptorChain);

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

    return promise;
  }

  var newConfig = config;
  while (requestInterceptorChain.length) {
    var onFulfilled = requestInterceptorChain.shift();
    var onRejected = requestInterceptorChain.shift();
    try {
      newConfig = onFulfilled(newConfig);
    } catch (error) {
      onRejected(error);
      break;
    }
  }

  // 当没有添加拦截器的时候会跳转到此处执行最终返回promise
  try {
    promise = dispatchRequest(newConfig);
  } catch (error) {
    return Promise.reject(error);
  }

  // 遍历响应拦截器数组
  while (responseInterceptorChain.length) {
    promise = promise.then(
      responseInterceptorChain.shift(),
      responseInterceptorChain.shift()
    );
  }

  return promise;
};

这里补充一个注意点,在真正使用 axios 的拦截器方法的时候,通过打印执行的结果可知,请求拦截器在执行上是逆序执行的,也就是其输出结果是从下往上依次输出的,而响应拦截器在执行上是顺序执行的,也就是其输出结果是从上往下依次输出。

出现这样的情况的原因在于:在 Axios.js 文件中对于拦截器方法的处理是分两种情况的,对于请求拦截器而言,其加入请求拦截器数组是使用 unshift()方法,也就是不断添加在数组的头部,类似栈的方式,最后再将完整的请求拦截器数组再通过 unshift()追加到 chain 数组的头部,chain 数组也就是包含请求拦截器,发送请求的 dispatchRequest()方法和响应拦截器的最终完整的执行数组。

而对于响应拦截器而言,其每一个拦截器项都是通过 push()方法推入数组的,类似队列的方式,最终也是通过 concat()方法将响应拦截器数组拼接到 chain 数组的末尾,所以最终就呈现出请求拦截器逆序输出,而响应拦截器顺序输出的情况

通过上述对于 Axios.js 文件的解析,我们可以看到,这个文件里 Axios.prototype.request()也就是我们最终可以实现发送请求的本质函数做的事情总结起来无非是以下几点:

1. 处理请求所需的配置项,将默认配置项和传入的新参数进行合并,最终生成完整的配置项config
  1. 创建请求拦截器数组,并且通过 unshift()依次将我们使用的拦截器函数推入数组,形成一个请求拦截器数组等待遍历执行
  2. 创建 chain 数组,添加两个数组项,chain[0]为 dispatchRequest()方法,也就是实现请求发送的核心方法,chain[1]为 undefined,用于占位符,之所以需要添加一个占位符的原因在于在遍历数组执行都是通过 promise.then()方法来实现的,而对于 then()方法而言,默认是拥有两个参数的,第一个是 onFulfilled 表示成功的回调,而第二个是 onRejected 表示失败的回调,而我们将 disptachRequest()方法作为成功的回调后,需要一个占位符来作为失败回调
  3. 创建响应拦截器数组,并且通过 push()依次将我们使用的拦截器函数推入数组,形成一个响应拦截器数组等待遍历执行
  4. 判断是否存在请求拦截器,如果存在,则将请求拦截器数组拼接到 chain 数组头部,然后遍历整个 chain 数组输出请求拦截器方法以及发送请求方法,最终返回一个 promise,也就是在使用时我们可以通过 axios().then()来获取请求结果的原因
  5. 遍历输出响应拦截器

接下来我们对其中两个非常重要的方法进行解析,一个是拦截器函数的实现,一个是请求发送的 dispatchRequest()方法的实现

首先是拦截器函数

// InterceptorManager.js

// 可以看到,最原始的InterceptorManager方法内部结构非常简单,只有一个handlers的初始空数组,作用是用来存档我们添加的拦截器函数的
function InterceptorManager() {
  this.handlers = [];
}

// 请求拦截函数和响应拦截函数
//可以看到关于拦截器执行函数use方法其实是定义在拦截器对象的原型上的,所以我们在使用时,就可以通过axios.request.use()来添加一个请求或响应的拦截器函数了
// 在use方法内部,我们可以看到通过push方法,把我们添加的拦截器函数中成功和失败的回调组成一个对象的形式添加到handlers数组当中,虽然最终执行的结果对于请求拦截器而言是逆序的,但由于我们每一次添加的都是一个对象整体,故每个拦截器内部的成功和失败回调都会是依次顺序存放的
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
    synchronous: options ? options.synchronous : false,
    runWhen: options ? options.runWhen : null,
  });
  return this.handlers.length - 1;
};

// 移除拦截器方法,即可以通过数组的下标值来移除handlers中对应的数组项
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

// 定义在拦截器对象原型上的遍历方法,用于遍历拦截器数组中的数组项
// 由于这个遍历方法是直接定义在拦截器对象的原型上,所以在实际使用中,也就是在Axios.js文件中遍历拦截器数组时,可以直接通过this.interceptors.response.forEach()来遍历
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

// 拦截器相关的核心文件到此就结束了,可以看到其实对于拦截器函数而言,其内部以及在原型上的方法只有三个,并且其实现也相对比较简洁

然后是请求发送的 dispatchRequest()方法

// dispatchRequest.js

// 抛出异常函数,当存在cancelToken用于取消请求的时候使用
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }

  if (config.signal && config.signal.aborted) {
    throw new Cancel("canceled");
  }
}

module.exports = function dispatchRequest(config) {
  // 取消请求,cancelToken,即抛出异常终止函数执行
  throwIfCancellationRequested(config);

  // 确保请求头信息存在
  config.headers = config.headers || {};

  // 格式化请求数据
  config.data = transformData.call(
    config,
    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];
    }
  );

  // 适配器,如果未传入,则使用默认适配器,目的是用来建立ajax发送请求
  // 也就是说,当我们未传入适配器相关方法时,默认使用的是xhrAdapter()方法来建立一个原生的AJAX请求,梳理到这里我们可以看到,最终通过一层一层的解析下来,之所以调用axios对象并且通过传参或者其原型上的方法可以向后端发送不同类型的请求,其实就是在dispatchRequest()方法中使用了adapter().then()并且将相应数据作为返回值返回,这样就可以在外层通过axios().then()方法来获取到响应的结果值了,而adapter()方法其实就是对于原生AJAX以及xhr对象的封装,所以不论如果处理,虽然axios使得我们发送请求更加的方便简洁,但是其归根结底依旧是对于原生AJAX做了更加完善的封装和处理。
  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(
    function onAdapterResolution(response) {
      throwIfCancellationRequested(config);

      // 格式化响应数据
      response.data = transformData.call(
        config,
        response.data,
        response.headers,
        config.transformResponse
      );

      return response;
    },
    function onAdapterRejection(reason) {
      if (!isCancel(reason)) {
        throwIfCancellationRequested(config);

        if (reason && reason.response) {
          reason.response.data = transformData.call(
            config,
            reason.response.data,
            reason.response.headers,
            config.transformResponse
          );
        }
      }

      return Promise.reject(reason);
    }
  );
};

到这里,我们从入口文件开始一步一步将一个 axios 对象的创建过程梳理完成,可以看到,虽然我们最终使用的 axios 看起来非常的简单,但是其内部是对原生 AJAX 做了非常多且复杂的封装,最终才暴露出一个简洁易懂的请求发送的方法,其实对于整个 axios 项目,还有关于适配器方法的处理相关的adapters文件夹以及其中存在的两个适配器文件http.jsxhr.js,而处于同级的cancel文件夹则是用来实现取消请求发送的,而在helpers文件夹中,作者封装了大量的适用于 axios 库本身可以使用的公共函数

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Axios源码解析(部分) 的相关文章

随机推荐

  • 【脚本】echo 输出赋值给变量

    链接 xff1a http zhidao baidu com link url 61 FMhso6Hf4eeRQN7p2qqzLOBAYPwh6yMJCWOvgmBFTDYWAEZ9ceuREtWhggxtcYG1iBhaJgqrcU7ad
  • 联邦学习 - 基础知识+白皮书+杨强教授讲座总结+同态加密+ 差分隐私

    联邦学习 兴起原因概念分类横向联邦学习纵向联邦学习联邦迁移学习 优势系统架构联邦学习与现有研究的区别联邦学习与差分隐私理论的区别联邦学习与分布式机器学习的区别联邦学习与联邦数据库的关系 联邦学习的最新发展及应用 xff08 2019第四届全
  • boomlab 实验 炸弹实验 系统级程序设计 CMU

    MENU boomlab还有30s到达实验1Step1 反汇编vim大法检查boom原因gdb调试出结果examinequit 实验二分析汇编语言ENDING 实验三答案 实验四func4 实验五实验六gdb调试 答案汇总ENDING问题解
  • CSAPP Lab:attacklab

    大小尾端 首先关于这个 xff0c 我一直没记清楚 xff0c 所以做个总结 xff1a 在裘宗燕翻译的 程序设计实践 里 xff0c 这对术语并没有翻译为 大端 和小端 xff0c 而是 高尾端 和 低尾端 xff0c 这就好理解了 xf
  • Advances and Open Problems in Federated Learning 总结翻译

    摘要 联邦学习 FL 是一种机器学习设置 xff0c 在这种设置中 xff0c 许多客户 例如移动设备或整个组织 在中央服务 器 例如服务提供商 的协调下协作地训练模型 xff0c 同时保持训练数据分散 FL体现了集中数据收集和最 小化的原
  • Multi-Center Federated Learning

    Multi Center Federated Learning Motivation 现有的联合学习方法通常采用单个全局模型来通过汇总其梯度来捕获所有用户的共享知识 xff0c 而不管其数据分布之间的差异如何 但是 xff0c 由于用户行为
  • No Fear of Heterogeneity: Classifier Calibration for Federated Learning with Non-IID Data

    No Fear of Heterogeneity Classifier Calibration for Federated Learning with Non IID Data Existing Methods for non IID da
  • Three scenarios for continual learning

    Three scenarios for continual learning Standard artificial neural networks suffer from the well known issue of catastrop
  • MQ2烟雾传感器

    1 MQ 2气体传感器所使用的气敏材料是在清洁空气中电导率较低的二氧化锡 SnO2 当传感器所处环境中存在可燃气体时 xff0c 传感器的电导率随空气中可燃气体浓度的增加而增大 使用简单的电路即可将电导率的变化转换为与该气体浓度相对应的输出
  • alembic

    alembic是sqlalchemy的作者开发的 用来做ORM模型与数据库的迁移与映射 alembic使用方式跟git有点了类似 xff0c 表现在两个方面 xff0c 第一个 xff0c alembic的所有命令都是以alembic开头
  • VScode远程免密登录

    安装配置python环境 xff1a 用VScode配置Python开发环境 xiaoj wang 博客园 cnblogs com VScode免密登录远程服务器 VS code ssh免密登陆 1 xff09 windows 下 xff0
  • linux虚拟机和主机能相互ping通,linux却不能访问外网

    linux虚拟机和主机能相互ping通 xff0c linux却不能访问外网 下面是试错过程 修改ifcfg eth0 xff08 名字可能不一样 xff09 vi etc sysconfig network scripts ifcfg e
  • 树莓派:树莓派的各个引脚

    由于第一次接触树莓派 xff0c xff0c xff0c emmmm xff0c 仔细写 xff0c 奥里给 3 3V 5V xff08 VCC xff09 xff1a 显然是电源正极啦 GND xff1a 接地用 xff0c 负极负极负极
  • 不分类工具:sd卡格式化工具安装教程

    下载地址 xff1a https www sdcard org downloads formatter 4 eula windows 进入上面这个链接 xff0c 你会看到满上面都是字 xff0c 有一个download xff0c 点完还
  • 不分类工具:Win32 DiskImager安装教程

    下载地址 xff1a http sourceforge net projects win32diskimager 这个也是很普普通通的下载安装 1 直接 download 2 双击安装文件 xff0c 弹出如下框 xff0c 选择我同意 x
  • Meta-Learning: Learning to Learn Fast

    Meta Learning Learning to Learn Fast 元学习 学习如何学习 译 原文 本文与原文基本没有区别 xff0c 仅供个人学习记录 电子笔记本 前言 xff1a 元学习解决 xff1a 遇到没有见过的任务 xff
  • 解决 Docker 容器时间与本地时间不一致的问题

    Linux 通过 Date 命令查看系统时间 xff0c 得到如下结果 xff1a root 64 iZ8vbg6m7f5ntzibw3t4huZ date Mon Aug 26 12 24 58 CST 2019 但是在 Docker 容
  • 记录ssh 和vnc命令

    ssh windows是客户端 linux是服务端 在windows powershell 输入 ssh rikirobot 64 192 168 x xxx xff08 ip地址 xff09 VNC Viewer 参考文章 xff1a 1
  • Redux源码解析(部分)

    相信用过React的小伙伴对于Redux一定不陌生 xff0c A Predictable State Container for JS Apps xff0c 这是官方文档对于Redux的定义 xff0c 即一款适用于JS Apps的可预测
  • Axios源码解析(部分)

    从 Github 上把 Axios 项目的 master 分支拷贝到本地 xff0c 用编辑器打开项目目录 首先我们先解析一下整个 Axios 项目的一些关键的文件结构 对照项目的文件目录 xff0c 梳理一下其中的一些关键文件夹以及它的作