axios源码解读(上)

1,113 阅读9分钟

通过源码的学习提升自己的编码能力和理解源码里面的设计模式,最终通过自己的理解,然后模仿做一个简易功能版本的轮子出来。希望通过这个源码系列来监督自己学习源码。

1. axios仓库地址及版本

这一次分析的axios源码仓库地址,版本是0.20.0,因为看源码过程中会对源码中加入自己的注释,所以特意保存到自己的仓库里面,所有的分析记录都在这个仓库里,有需要的读者可以下载,地址:axios源码分析地址

2. axios源码目录

克隆axios源码地址到本地,然后对目录进行分析。源码学习中需要对代码进行断点调试,那么这个时候就要知道项目代码是怎么运行起来的。这里axios目录下有一个源码贡献须知的md文件CONTRIBUTING.md,所以直接打开它就能知道项目是怎么运行起来的。

项目目录.png

3. axios运行和调试

这里直接上运行命令,具体解释可以看下面的文字说明。

// 1. 克隆官方仓库(这里可能需要自备梯子)
git clone https://github.com/axios/axios.git 
// 2. 启动监听文件变化命令需要安装grunt,如果有的话跳过这一步
npm i -g grunt
// 3. 启动实时监听
grunt watch:build
// 4. 启动调试页面, 打开http://localhost:3000
npm start

监听lib目录文件变化

CONTRIBUTING.md文件知道项目的构建方式是通过grunt + webapck,运行axios项目代码需要全局安装grunt (npm i -g grunt)。通过grunt watch:build命令,实时监听lib文件夹内的文件变化,然后触发打包,最终生成dist目录的内容(axios.js、axios.min.js)。

界面调试

通过实时监听文件变化的命令就可以打包dist目录内容,但是这里并没有引用这个目录下js文件的静态页面,所以还需要一个静态页面方便调试。npm start命令可以启动3000端口返回一个静态页面(具体的页面代码在sanbox目录下的client.html),这个静态的页面引入js文件的就是利用grunt watch:build打包出来的,然后我们就可以利用这个页面对axios源码进行调试。

除了npm start还可以用npm run examples命令来调试项目提供的例子。但如果使用这个例子进行调试的时候需要改变一下emamples/server.js文件的80行,具体修改如下:

// Process axios itself
if (/axios\.min\.js$/.test(url)) {
  pipeFileToResponse(res, '../dist/axios.js', 'text/javascript'); // 把原来的axios.min.js换成是axios.js,这里其实就是页面访问axios.min.js的时候返回的是axios.js的文件内容,方便调试查看
  return;
}

经过执行上面的命令,这个时候可以通过访问http://localhost:3000页面来配合实时监听lib目录来调试axios的源码。

4. axios源码初始化

4.1. 主要源码项目结构

项目里面是用grunt+webpackgrunt主要负责的是监听文件的变化,然后依赖解释,打包都是由webpack来完成,所以我们要看webpack.config.js文件。

{
  ...
  entry: 'index.js',  // 项目入口文件
}

通过webpack配置文件可以知道项目的入口是index.js,index.js文件很简洁只有一个引入,那就是lib/axios.js文件。

4.2. 初始化lib/axios.js

初始化代码时,引用的代码较多,我们主要集中关心其中几个即可:

  1. 项目引入的工具类utils(extend,forEach)Axios构造函数,bind函数,mergeConfig函数;
  2. 生成实例的方法createInstance
  3. axios身上的cancel相关的方法;

接下来就按照代码初始化的顺序来跟踪项目代码,相关的变量数据的变化读者可以进行断点调试观察。

4.3. 第一步:引入封装的函数

初始化阶段工具类等方法的引入。

// 文件位置:lib/axios.js

// 严格模式
'use strict';

// 工具类
var utils = require('./utils');
// 引入bind方法
var bind = require('./helpers/bind');
// Axios构造函数
var Axios = require('./core/Axios');
// mergeConfig(config1, config2); 把config2的属性合并到config1上,返回新的对象
var mergeConfig = require('./core/mergeConfig');
// 默认配置
var defaults = require('./defaults');

4.3.1. bind方法

bind方法(和ES5bind方法基本一致,可能就是参数位置不太一样),这里传入两个参数,fn就是需要绑定this的函数,thisArg就是this指向的对象。

'use strict';

// bind函数:绑定fn函数内部this到thisArg参数对象,然后返回一个wrap函数
module.exports = 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);
  };
};

这里有一个疑问点:就是apply是支持arguments这样的伪数组,但是源码上还是把arguments转成是数组再调用apply,有知道的读者可以回答一下这个我的疑问。

调用bind的例子:

// 引入bind方法
var bind = require('./helpers/bind');

// 下面是调用bind方法的例子
var obj = {one: 1};
function fn(two) {
  console.log(this.one + two);
}

// 把fn函数上的this绑定在obj对象上,并返回一个wrap函数
var bindFn = bind(fn, obj); 
bindFn(2); // 结果:3

4.3.2. forEach方法

// obj:需要遍历的对象或者数组
// fn: 遍历后的每一个元素作为参数传给fn函数调用
function forEach(obj, fn) {
  // 如果遍历的对象为空,则跳出方法
  if (obj === null || typeof obj === 'undefined') {
    return;
  }

  // 如果类型不是object的元素,则添加到一个新数组里
  if (typeof obj !== 'object') {
    /*eslint no-param-reassign:0*/
    obj = [obj];
  }
	
  // isArray判断obj是否为数组
  if (isArray(obj)) {
    // 遍历数组里面的每一个元素,调用fn函数并且把遍历的元素传入
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // 对象遍历(for in会遍历原型链上的其他属性)
    for (var key in obj) {
      // 忽略当前继承属性
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        // 遍历的每一个属性值作为参数传入fn
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

4.3.3. extend方法

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

遍历对象或者数组b里面每一个属性或者元素,把他们添加到a对象上,如果b里面的每一个元素是函数,那么就绑定thisthisArg上。

调用utils.extend方法例子:

var axios = {};
var request = {
  "get": function () {
    console.log(this.config);
  },
  "post": function () {
    console.log(this.config);
  }
};
var obj = {
  config: 'config属性'
};

/*
 * 结果: axios = {
 *		"get": function() {
 *			console.log(this.config);
 *		}, 
 *		"post": function () {
 *			console.log(this.config);
 *		}
 *	}
 * 这里的this绑定在obj对象上了
**/
utils.extend(axios, request, obj);

4.4. 第二步:实例化

4.4.1. 调用顺序

  • createInstance方法;(lib/axios.js)
  • Axios构造函数和两个utils.forEach方法;(lib/core/Axios.js)
  • InterceptorManager构造函数;(lib/core/Axios.js)
  • 最后的bindextend方法的绑定;(lib/axios.js)

代码中初始化的时候执行了var axios = createInstance(defaults);,所以我们这一步重点关注createInstance函数。

/**
 * 创建axios实例
 * @param {*} defaultConfig 实例上的默认配置
 * @returns Axios实例
 */
function createInstance(defaultConfig) {
  // 返回Axios实例 {defaults, interceptors}
  var context = new Axios(defaultConfig);
  
  ...
  
  return instance;
}

4.4.2. 调用Axios构造函数

var context = new Axios(defaultConfig);,这里使用了new操作符调用了Axios构造函数,返回值{defaults, interceptors},并且在context对象上绑定了Axios原型链上的request方法,还有getpostdeletepatchput等方法,这些方法本质上其实还是调用request方法。因为初始化的时候还没有调用这些请求方法,所以会把这个request方法放到下一个篇章。

构造函数Axios:

/**
 * Axios类
 * 配置对象绑定在defaults属性
 * 拦截器实例(请求,响应)绑定在interceptors属性
 * @param {*} instanceConfig 实例上的配置
 */
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  // 拦截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

构造函数Axios上面初始化的时候绑定默认配置defaults,并且在拦截器interceptors扩展requstresponse属性。

4.4.3. 拦截器构造函数InterceptorManager

这个构造函数内有三个方法,分别是use(添加)eject(根据id清空)forEach(遍历)

'use strict';

var utils = require('./../utils');

function InterceptorManager() {
  this.handlers = [];
}

// 往handles数组末尾添加元素{成功回调函数,失败回调函数},并且把添加后对应的索引值返回
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

// 根据id(索引值)清空handles数组上的元素
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

// 遍历handles数组上的元素,把它们传给fn执行,这里过滤掉为null的元素。
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

module.exports = InterceptorManager;

4.4.4. bind函数和extend函数

让我们回到createInstance函数初始化的地方,new操作符调用了Axios构造函数返回的对象context,此时它上面拥有了各种请求方法(get、post、put、delete),而且也拥有了拦截器属性(interceptors)和默认配置属性(defaults),但是createInstance函数还没有结束,我们继续往下看

/**
 * 创建axios实例
 * @param {*} defaultConfig 实例上的默认配置
 * @returns Axios实例
 */
function createInstance(defaultConfig) {
  // 返回Axios实例 {defaults, interceptors}
  var context = new Axios(defaultConfig);
  
  // 把request方法里面的this绑定到实例对象context({defaults, interceptors})上
  var instance = bind(Axios.prototype.request, context);

  // instance是原型链上的request函数(this绑定在contenxt上)
  // 这里的extend函数就是把原型链上的所有请求方法都添加都instance函数上(js函数也是对象,也就是往函数上添加其他属性),并且把this都绑定在实例对象context上
  // 目的:使instance既可以执行,也可以调用属性方法(get、post、put、delete...)
  utils.extend(instance, Axios.prototype, context);

  // instance函数的基础上扩展defaults,interceptors属性
  utils.extend(instance, context);

  // 返回instance函数
  // 现在的instance函数身上绑定了get,post,put,delete,request...的方法,还有defaults,interceptors属性
  return instance;
}

createInstance最后返回的instance,它本身就是request函数,然而instance身上还有其他的属性方法(get、post、put、delete),还有实例上的属性defaults和拦截器interceptors。所以项目引入axios库的时候,既可以调用axios()、axios.get()、axios.post()发送请求,也可以通过axios.defaults获取默认属性,还可以通过axios.interceptors对拦截器进行调用。

5. 总结

本章详细的讲解了axios库的源码运行和调式,还有梳理了axios初始化时候调用的函数,知道axios本质上就是一个函数,既可以当作函数调用axios(),也可以当作对象使用axios.get()。但这里没有把request具体逻辑和cancel方法进行分析,后续会把这部分给补充完整。

如果读者发现有不妥或者可以改善的地方,欢迎在评论区指出。如果觉得写得不错或者对你有所帮助,可以点赞、评论、转发分享,谢谢~