零:axios目录结构与入口文件

本系列内容基于axios0.18.0版本,只对核心内容进行梳理,不会具体到每一个方法细节。至于一些辅助方法等内容,只会在碰到的时候说下这个方法做了什么,不会单独去看辅助方法。新版本的axios对于一些部分进行了修改,但大致是一样的。

axios的适配器adapter分别对xhrhttp做了处理,我们只关注用于浏览器端的xhr,对应用于nodehttp内容不做考虑。

目录结构

通过webpack.config.js文件发现项目的入口文件是跟目录下的index.js文件

index.js文件仅仅是引入了lib/axios.js文件并导出,所以我们对于axios源码内容的分析就是对于lib目录下的文件的分析,对于其他的示例、测试用例等不关注。

1
2
// index.js
module.exports = require('./lib/axios');

目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
lib	// 源码目录
|---- adapters // 适配器目录
| |---- xhr.js // 浏览器xhr
| |---- http.js // node环境http
|---- cancel // 取消请求相关功能目录
| |---- Cancel.js
| |---- CancelToken.js
| |---- isCancel.js
|---- core // 核心代码目录
| |---- Axios.js // Axios类,用于创建axios实例
| |---- createError.js // 配合enhanceError返回错误
| |---- dispatchRequest.js // 封装发送请求的方法
| |---- enhanceError.js
| |---- InterceptorManager.js // 拦截器
| |---- settle.js
| |---- transformData.js // 数据转换
|---- helpers // 一些辅助函数
|---- axios.js // 真正的入口文件
|---- defaults.js // 默认配置
|---- utils.js // 一些公共方法

入口文件

axios与axios.create

前边我们已经分析过了,跟目录下的index.js只是导入了lib/axios.js的导出内容并向外导出,所以在这里我们开始看一下lib/axios.js文件:

我们先摘两行代码出来看一下,至于createInstance方法是什么暂时先不需要过多关注,它只是创建了一个axios实例然后做了一些继承操作后将这个实例返回出来

1
2
3
4
5
6
7
// lib/axios.js
// ....
var axios = createInstance(defaults);
// ....
module.exports = axios;
// 用于满足在typescript中使用默认导入
module.exports.default = axios;

我们可以看到lib/axios.js文件中最后导出了创建的axios实例,而根目录下的index.js文件导入了这个实例并向外导出,所以我们在项目中通过import导入的就是这个axios实例啦。

我们知道axios实例可以通过axios.create()方法创建新的实例,这个是怎么实现的呢?

1
2
3
4
axios.create = function create(instanceConfig) {
// utils.merge可以合并配置项
return createInstance(utils.merge(defaults, instanceConfig));
};

很明显,axios.create内部是调用了createInstance方法来实现的,而且我们又知道axios也是通过createInstance方法创建的,那是不是可以认为axiosaxios.create()创建的实例是一样的呢?

其实只看这些代码的话,可以说他们是基本一样的,只是axios.create可以传入一些额外配置项。

但是看后边的代码就会发现还是有很大不一样的,不需要关注这些方法用来做什么,只看懂这个流程即可:

1
2
3
4
5
6
7
8
9
10
11
12
axios.create = function create(instanceConfig) {
return createInstance(utils.merge(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');

可以看到,像是CancelCancelToken等方法是在axios创建之后手动添加到axios身上的,而不是在createInstance中添加到axios上的,那么也就是说,CancelCancelToken这些方法可以通过axios调用,但是通过axios.create创建的实例是不能调用这些方法的。

axios.all和axios.spread

我们现在回过头看一下这几个方法,CancelCancelTokenisCancel这几个用于取消请求的方法我们放到后边看这几个文件的时候再说,all方法其实就是用了Promise.all方法,没什么太多好说的,直接看spread方法吧,这个方法是个高阶函数,而且还让我感觉有那么点儿函数柯里化的意思,主要是用来打散数组参数类似于apply的效果。

allspread方法在平常的应用场景中使用不那么多,我们来看一下。

spread方法实现如下:

1
2
3
4
5
module.exports = function spread(callback) {
return function wrap(arr) {
return callback.apply(null, arr);
};
};

假设我们有一个函数f接受三个参数

1
function f (x, y, z){}

我们希望传递一个有三个元素的数组作为参数,可以这么做

1
2
var args = [1, 2, 3]
f.apply(null, args)

apply方法不仅可以改变this指向,还可以打散数组参数传入。

那么现在我们有了spread方法怎么实现这个功能呢?

1
spread(f)(args)

看到这里,大家是不是不太清楚spread方法的意义何在,其实spreadall方法配合使用有不错的效果。

比如我们需要同时请求多个接口且需要等待多个接口都返回了才能进行下一步操作,这时如果同步进行会影响运行效率,所以可以使用这两个方法完成高并发请求,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 两个axios请求
function fetch1(){
return axios()
}
function fetch2(){
return axios()
}
// 使用axios.all等两个请求都结束才向后运行,返回一个数组
axios.all([fetch1(), fetch2()])
.then(axios.spread(function(res1, res2){
console.log(res1) // fetch1的结果
console.log(res2) // fetch2的结果
}))

因为axios.all返回的是一个数组,是所有请求的结果,我们需要手动从数组中取值,这个情况下使用axios.spread打散结果数组取值会更加方便。

总结, axios.allaxios.spread方法适用于并发请求。

createInstance

现在整个lib/axios.js文件就剩下一个createInstance方法没有去看了,我们就看看这里究竟做了些什么。

这里用到了Axios类,我们会在下一节去详细介绍Axios类的内容,这里只会把必须要讲到的大概说一下能理清createInstance方法的功能即可。

先看一下createInstance方法做了什么:

1
2
3
4
5
6
7
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
utils.extend(instance, Axios.prototype, context);
utils.extend(instance, context);
return instance;
}

源码对这个函数的描述是Create an instance of Axios

我们根据最开始说过的var axios = createInstance(defaults);可以知道这个实例是使用默认配置创建的。函数内部第一行是通过new创建了一个实例context,最后一行是把实例返回出去,这两行不过多解释,我们专注于函数内的第二行、第三行和第四行。

  • var instance = bind(Axios.prototype.request, context);

先说Axios.prototype.request,它就是我们用来发请求的方法,这一行代码就是为了实现我们使用axios({method:'get',url:''})这样来发送请求,其实调用的就是Axios原型对象上的这个request方法,大家这么说应该能知道这个request方法大概是什么,其具体实现不在这里展开讲,留到下一节再说。

这里主要讲bind方法是怎么实现上边说的使用方式的。看到bind方法我们应该会想到js中用于永久改变this指向的bind方法,当然这里的bindaxios中封装过的并不是我们想的那个,其内部用到的是js中的apply方法,很意外吧。

先来看一下这个bind方法的源码

1
2
3
4
5
6
7
8
9
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);
};
};

看我们传入的参数,fn就是那个request方法,而thisArg就是我们通过new创建的Axios实例context。执行bind之后我们得到的instance就是这个wrap函数。在wrap函数内部先是处理了一下参数,因为arguments是一个类数组对象,并不是一个真正的数组,然后通过apply调用将this指向我们的实例context并传入参数。

所以,我们也可以对这行代码这么理解(只是这么理解,这是不对的):

1
instance(...args) = Axios.prototype.request.apply(context, arguments)
  • utils.extend(instance, Axios.prototype, context);

第三行和第四行都用到了extend这个方法,这个方法比较复杂,内部还用到了封装过的bind方法和forEach方法,其中的bind已经说过了,forEach还是用来循环的,接收两个参数,第一个是用来循环的内容,第二个是回调函数,把循环内容中每一个元素的值,键,元素本身传入回调函数执行。这里不做详细解释,后边把forEach的源码贴出来,感兴趣自己看一下。

我们先看下extend这个方法的内容

1
2
3
4
5
6
7
8
9
10
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;
}

第三行就是传递了第三个参数thisArg的情况,这行代码的作用就是把Axios.prototype的内容复制到instance上且方法的this指向context,所以可以说第三行是用来把原型对象上的内容复制到instance

  • utils.extend(instance, context);

第四行这样来看也就比较简单了,是把context的内容复制到instance身上,所以可以说第四行是把构造函数上的内容复制到instance

贴一下forEach源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function forEach(obj, fn) {
if (obj === null || typeof obj === 'undefined') {
return;
}
if (typeof obj !== 'object') {
obj = [obj];
}

if (isArray(obj)) {
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
}
}

axios.js总览

现在整个入口文件就只剩一行代码没看了,如下:

1
axios.Axios = Axios;

这行代码的意义就是暴露Axios类来允许类继承,不过多解释。

这样我们把整个入口文件就说完了,把源码贴出来看一下吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
'use strict';
// 引入
var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var defaults = require('./defaults');

// 创建axios实例
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
// 可以直接把实例当作函数来调用发送请求
var instance = bind(Axios.prototype.request, context);
// Axios原型内容拷贝
utils.extend(instance, Axios.prototype, context);
// Axios构造函数内容拷贝
utils.extend(instance, context);
return instance;
}

// 创建实例
var axios = createInstance(defaults);
// 暴露Axios类
axios.Axios = Axios;

// create工厂函数
axios.create = function create(instanceConfig) {
return createInstance(utils.merge(defaults, instanceConfig));
};

// 取消请求的相关内容
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

// 用于处理并发请求的方法
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');


// 导出
module.exports = axios;
// 适配typescript
module.exports.default = axios;

那么对于入口文件的内容就说到这里了。

作者

胡兆磊

发布于

2022-10-08

更新于

2022-10-23

许可协议