axios源码解析-Axios对象
之前说过,axios中,axios本身是一个函数,但它绑定了Axios构造函数的所有方法,那么,Axios这个构造函数究竟有那些属性和方法呢,在axios的源码中,有专门的一个文件Axios.js去编写这个构造函数,那么我们先来看一下源码中是如何写的
// 检验版本的对象
var validators = validator.validators;
/**
* Create a new instance of Axios
*
* @param {Object} instanceConfig The default config for the instance
*/
// Axios的处理流程:先执行请求拦截器,
// 然后执行transfromDataResquest,xhr,transfromDataRespones(这三部分在dispatchRequest这个流程中执行)
// 最后执行响应拦截器
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
可以看出,Axios这个对象本身非常简单,将参数设置为默认配置,然后将本身的拦截器通过InterceptorManager的实例化得到请求拦截器和响应拦截器,其实这也就是axios本身最重要的作用,在主体中只包含了最重要的骨架。之后,在Axios的原型链上挂在了有一些方法,其中最重要的是处理请求的rquest
Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
// 对传入配置的处理
// 这里可以处理不同的请求形式:axios('url',config)或者axios(config)
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
// 完善并补充config配置(跟自己之前设置的default相比进行添加或覆盖,合并两个配置)
config = mergeConfig(this.defaults, config);
// Set config.method
if (config.method) {
// 自动将请求的方法转化为小写
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
// 没有在配置中设置请求方法使用默认配置
config.method = this.defaults.method.toLowerCase();
// 没有设置请求方法且没有默认配置,默认使用get方法
} else {
config.method = 'get';
}
var transitional = config.transitional;
// 进行版本检验,1.0.0版本后,transitional这个属性被移除
if (transitional !== undefined) {
validator.assertOptions(transitional, {
silentJSONParsing: validators.transitional(validators.boolean, '1.0.0'),
forcedJSONParsing: validators.transitional(validators.boolean, '1.0.0'),
clarifyTimeoutError: validators.transitional(validators.boolean, '1.0.0')
}, false);
}
// filter out skipped interceptors
// 拦截器链
var requestInterceptorChain = [];
// 是否为同步函数
var synchronousRequestInterceptors = true;
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);
});
上面这部分是做了一些前置处理,比如对不同的请求格式和不同版本进行适配,而后面这部分对于链式调用的处理则是axios源码中最经典的一部分
var responseInterceptorChain = [];
// 注意这里的foreach方法是拦截器原型链上的foreach方法
// 这个方法内部会遍历所有拦截器
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
// 将每个响应拦截器加入到执行链的最后端
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
var promise;
// 如果拦截器需要异步处理
if (!synchronousRequestInterceptors) {
// undefined用于占位,保证回调函数两个一组可以成对处理
// 这里的dispatchRequest是处理请求的核心代码
// 在dispatchRequest中会检验是否取消请求,适配器处理,并利用transfrom在发送请求前对数据进行处理
var chain = [dispatchRequest, undefined];
// 将整个请求拦截器添加到执行链的前端,将响应拦截器整个添加到执行链的后端
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain.concat(responseInterceptorChain);
// 请求拦截器最先执行,可以拿到全部的config
promise = Promise.resolve(config);
while (chain.length) {
// 两个为一组(正确和错误回调)逐个shift,解决回调地狱代码
// 注意这里处理到dispatchRequest就会执行核心的异步请求,然后继续执行响应拦截器
// 这里的参数也会利用promise逐层传递,由于链式调用的各个部分都是promise,相当于进行了链式调用
promise = promise.then(chain.shift(), chain.shift());
}
// 最终返回一个promise,这个相当于是经过处理后,我们最后使用axios时的异步函数,它的。then也就是我们最后使用的then
return promise;
}
};
我们来梳理一下这部分的基本逻辑,axios将所有请求拦截器加入到了核心处理方法dispatchRequest的前面,把所有的响应拦截器加入到核心处理方法的后面,这里分了两种情况进行处理,如果有有拦截器是异步的方法,那么就调用异步的处理方法,将保存了所有方法的数组两个一组进行调用,前面的作为成功的回调函数,后一个作为失败的回调函数(这也是之前放入数组的顺序),当进行到dispatchRequest时,由于提前放入了一个undefined作为占位符,因此这时候只会处理dispatchRequest这个函数,之后再用同样的方式依此调用响应拦截器,最后返回一个Promise对象,这就是我们平时使用axios时拿到的promise对象,我们只要调用then方法就可以拿到最后经过响应拦截器处理后的返回结果。这个过程中,由于最开始定义的promise拿到了参数,因此在每个拦截器都会由于链式调用(这里相当于用数组的方式实现了链式调用)而拿到上一层处理后的参数。最终实现了用拦截器处理整个数据的目的
如果是响应拦截器和请求拦截器都是同步的函数的话,我们就没有必要使用更耗费性能的异步函数了,因此,对于同步的拦截器,也做了类似的处理
首先要注意,只有所有拦截器都是同步的,才能用同步进行处理,只要有一个拦截器是异步的,就会因为处理顺序混乱而产生错误。另外,同步拦截器只能处理请求拦截器的部分,因为dispatchRequest部分采用Promise封装AJAX,返回的是一个Promise对象,所以必须采用异步的方式处理响应拦截器
// 当拦截器为同步时,进行处理(如果是异步的话拦截器执行链已经执行完毕)
var newConfig = config;
// 首先处理请求拦截器
while (requestInterceptorChain.length) {
var onFulfilled = requestInterceptorChain.shift();
var onRejected = requestInterceptorChain.shift();
try {
// 递归,每次调用成功的回调函数更新配置
newConfig = onFulfilled(newConfig);
} catch (error) {
// 否则调用处理异常的函数
onRejected(error);
break;
}
}
// 注意在早期版本中。dispatchRequest是放在Promise中进行处理的,相当于在微任务中执行请求
// 由于微任务的创建是在promise链构建之前,因此这种方式会增加进行真正请求前的处理时间
//注意到这里用try-catch微任务不会过早创建,也就解决了微任务过早创建,宏任务过长或异步阻塞导致请求延时处理的情况
// 相当于之前是先创建微任务,再执行请求,
// 现在将请求放入宏任务中,先执行请求再创建微任务,利用微任务创建时间和网络通信时间的重合,缩短响应时间
try {
promise = dispatchRequest(newConfig);
} catch (error) {
return Promise.reject(error);
}
// 用同样的方式处理响应拦截器,使其链式调用
while (responseInterceptorChain.length) {
promise = promise.then(responseInterceptorChain.shift(),
responseInterceptorChain.shift());
}
return promise;
这里我们使用递归的方式处理同步的请求拦截器,因为我们必须每次调用拦截器后更新congfig,因此源码中采用递归的方式解决config更新的问题。同时,每次也是shift出数组的两个函数,用try-catch的方式处理使用拦截器的成功与失败,保证了两个函数一组出数组的统一性。后面用异步处理dispatchRequest和响应拦截器的部分跟之前基本类似,就不再赘述了,注意dispatchRequest本身是一个同步方法,但他返回一个异步对象,因此可以直接写在try里面,不需要用promise处理
我们可以看到拦截器加入的规则:由于请求拦截器用unshift添加,因此后添加的先调用;响应拦截器用push添加,因此先添加的先调用
之后有一个geturl的方法,用于返回完整的url,主要是要使用正则
// 获取完整url
Axios.prototype.getUri = function getUri(config) {
config = mergeConfig(this.defaults, config);
return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};
下面这部分,是axios为了简化我们的写法,使用工厂模式为我们批量创建的多个方法,这些方法本质上还是使用request进行处理,但利用合并配置的mergeConfig函数支持我们通过结构赋值的方式进行传参或多种格式进行传参,也就是我们平时最常用的版本
注意这里的foreach是until函数中自定义的foreach,不是JS自带的API
// 使用工厂模式:批量处理和添加多种方法,分为需要携带数据和不需要携带数据的
// 这里的作用相当于兼容了更多的写法格式,比如axios.get('url'),axios.post("url",data)等,本质上还是调用request方法
// Provide aliases for supported request methods
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
// 支持解构赋值
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: (config || {}).data //优先使用config中的参数,也可以不携带
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, data, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: data //优先使用传进来的参数
}));
};
});