从 Github 上把 Axios 项目的 master 分支拷贝到本地,用编辑器打开项目目录
首先我们先解析一下整个 Axios 项目的一些关键的文件结构
![image_1](https://i-blog.csdnimg.cn/blog_migrate/938e8a16f98b9bf6eb035cb36106befc.png)
//对照项目的文件目录,梳理一下其中的一些关键文件夹以及它的作用
// 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
- 创建请求拦截器数组,并且通过 unshift()依次将我们使用的拦截器函数推入数组,形成一个请求拦截器数组等待遍历执行
- 创建 chain 数组,添加两个数组项,chain[0]为 dispatchRequest()方法,也就是实现请求发送的核心方法,chain[1]为 undefined,用于占位符,之所以需要添加一个占位符的原因在于在遍历数组执行都是通过 promise.then()方法来实现的,而对于 then()方法而言,默认是拥有两个参数的,第一个是 onFulfilled 表示成功的回调,而第二个是 onRejected 表示失败的回调,而我们将 disptachRequest()方法作为成功的回调后,需要一个占位符来作为失败回调
- 创建响应拦截器数组,并且通过 push()依次将我们使用的拦截器函数推入数组,形成一个响应拦截器数组等待遍历执行
- 判断是否存在请求拦截器,如果存在,则将请求拦截器数组拼接到 chain 数组头部,然后遍历整个 chain 数组输出请求拦截器方法以及发送请求方法,最终返回一个 promise,也就是在使用时我们可以通过 axios().then()来获取请求结果的原因
- 遍历输出响应拦截器
接下来我们对其中两个非常重要的方法进行解析,一个是拦截器函数的实现,一个是请求发送的 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.js
和xhr.js
,而处于同级的cancel文件夹
则是用来实现取消请求发送的,而在helpers文件夹
中,作者封装了大量的适用于 axios 库本身可以使用的公共函数