Axios请求库学习(三):Axios核心代码分析
经过上文的学习,已经对axios源码有了一些了解,知道了axios对象中包含哪些属性和方法,以及最终导出的axios其实是一个函数。但是对其中实例的原理,以及各种方法的原理却没有深入,从这一篇文章就开始对这些内容的学习。
Axios.js
在这个文件中,主要内容可分为五个部分:
小模块的导入
Axios函数对象的声明
原型方法request和getUri
给请求方法设置别名
导出Axios
小模块的导入
// 工具函数 var utils = require('./../utils'); // 辅助函数 var buildURL = require('../helpers/buildURL'); // 拦截器对象 var InterceptorManager = require('./InterceptorManager'); // 派发请求方法 var dispatchRequest = require('./dispatchRequest'); // 合并配置 var mergeConfig = require('./mergeConfig'); // 校验方法 var validator = require('../helpers/validator'); 复制代码
Axios函数对象
在上一文章中,我们有提到够Axios实例的内容:defaults属性和interceptors属性,这两个属性的具体内容如下:
function Axios(instanceConfig) { this.defaults = instanceConfig; this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() }; } 复制代码
defaults = instanceConfig 为Axios对象的默认配置内容
interceptors 为拦截器对象,里面包含两个拦截器:请求拦截器和响应拦截器
原型方法getUri
整体代码
Axios.prototype.getUri = function getUri(config) { config = mergeConfig(this.defaults, config); return buildURL(config.url, config.params, config.paramsSerializer).replace(/^?/, ''); }; 复制代码
mergeConfig方法解析
mergeConfig方法用于合并默认配置和使用时传入的配置项。这部分的主要内容是在mergeConfig.js的后半部分
valueFromConfig2方法表示该属性的值是从传入的配置中获取的
defaultToConfig2方法表示如果传入的配置中该属性的值为undeifned,则从默认配置中获取,反之从传入配置中获取
mergeDirectKeys方法表示根据传入配置的类型去做不同的处理,内容为属性合并后的值
utils.forEach执行的目的就是将config中对应的属性通过执行mergeMap中对应属性的值(值是一个函数)来生成最终的值
module.exports = function mergeConfig(config1, config2) { config2 = config2 || {}; var config = {}; var mergeMap = { 'url': valueFromConfig2, 'method': valueFromConfig2, 'data': valueFromConfig2, 'baseURL': defaultToConfig2, 'transformRequest': defaultToConfig2, 'transformResponse': defaultToConfig2, 'paramsSerializer': defaultToConfig2, 'timeout': defaultToConfig2, 'timeoutMessage': defaultToConfig2, 'withCredentials': defaultToConfig2, 'adapter': defaultToConfig2, 'responseType': defaultToConfig2, 'xsrfCookieName': defaultToConfig2, 'xsrfHeaderName': defaultToConfig2, 'onUploadProgress': defaultToConfig2, 'onDownloadProgress': defaultToConfig2, 'decompress': defaultToConfig2, 'maxContentLength': defaultToConfig2, 'maxBodyLength': defaultToConfig2, 'transport': defaultToConfig2, 'httpAgent': defaultToConfig2, 'httpsAgent': defaultToConfig2, 'cancelToken': defaultToConfig2, 'socketPath': defaultToConfig2, 'responseEncoding': defaultToConfig2, 'validateStatus': mergeDirectKeys }; utils.forEach(Object.keys(config1).concat(Object.keys(config2)), function computeConfigValue(prop) { var merge = mergeMap[prop] || mergeDeepProperties; var configValue = merge(prop); (utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue); }); return config; } 复制代码
bindURL方法解析
bindURL方法用于对请求参数做处理,根据不同的参数形式去做不同的处理
如果参数为空,直接返回
如果参数是数组类型,则在属性名后面加一个字符串'[]',反之将属性值转为数组形式
对属性值进行遍历
最终将连接的呢绒再以&符号连接
如果是日期类型,则调用toISOString()方法,使用 ISO 标准返回 Date 对象的字符串格式;
如果是对象类型,则转为json字符串。
将结果中的属性名和属性值都以encode方法处理后使用等号(=)进行连接
如果没有params参数,则直接返回url
如果自带参数解析方法paramsSerializer,则通过paramsSerializer进行解析处理
如果params是URLSearchParams格式,则将参数转为字符串即可
如果不符合以上判断条件,则进行axios自己的参数处理操作
如果存在参数解析的值,则要对URL进行处理,去除掉哈希模式下的#号,再将参数通过?拼接上去
module.exports = function buildURL(url, params, paramsSerializer) { if (!params) { return url; } var serializedParams; if (paramsSerializer) { serializedParams = paramsSerializer(params); } else if (utils.isURLSearchParams(params)) { serializedParams = params.toString(); } else { var parts = []; utils.forEach(params, function serialize(val, key) { if (val === null || typeof val === 'undefined') { return; } if (utils.isArray(val)) { key = key + '[]'; } else { val = [val]; } utils.forEach(val, function parseValue(v) { if (utils.isDate(v)) { v = v.toISOString(); } else if (utils.isObject(v)) { v = JSON.stringify(v); } parts.push(encode(key) + '=' + encode(v)); }); }); serializedParams = parts.join('&'); } if (serializedParams) { var hashmarkIndex = url.indexOf('#'); if (hashmarkIndex !== -1) { url = url.slice(0, hashmarkIndex); } url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams; } return url; }; 复制代码
原型方法request
方法的代码太长,分作几部分介绍:
配置项处理
请求方式处理
对transitional属性进行版本校验
拦截器处理
配置项处理
if (typeof config === 'string') { config = arguments[1] || {}; config.url = arguments[0]; } else { config = config || {}; } config = mergeConfig(this.defaults, config); 复制代码
这样处理的原因在于传入配置参数的不同。
还记得在第一篇文章中介绍axios使用方式时的API方式吗?里面分为了两种使用方式。
axios(config)
axios({ method: 'get', url: 'xxx', data: {} }); 复制代码
axios(url,config) config可不传
axios('http://xxx'); axios('http://xxx',{ xxx }); 复制代码
这里的第一个判断条件就是axios(url,config)的情况,第二个是axios(config)的情况,最终将传入的配置和默认配置项进行合并。
请求方式处理
if (config.method) { config.method = config.method.toLowerCase(); } else if (this.defaults.method) { config.method = this.defaults.method.toLowerCase(); } else { config.method = 'get'; } 复制代码
由于前端传入时,请求方式大小写形式都有,所以要统一处理成小写形式,以及设置了默认的请求方式
对transitional属性进行版本校验
var transitional = config.transitional; if (transitional !== undefined) { validator.assertOptions(transitional, { silentJSONParsing: validators.transitional(validators.boolean), forcedJSONParsing: validators.transitional(validators.boolean), clarifyTimeoutError: validators.transitional(validators.boolean) }, false); } 复制代码
这段代码是对transitional 属性进行版本校验,会提示自某个版本之后移除该属性。
我在网上看到过validators.boolean会跟一个'1.0.0'版本号,但是我从gitee网站上clone的最新代码是没有,不晓得是哪里的改变了。
拦截器处理
这一部分,我也是看不太懂。在网上找到了仙凌阁大大的文章blog.csdn.net/qq_39221436…
复制了拦截器部分的解读代码,由于这部分代码经过一次重构,所以两种代码的展示都放到下面了。
重构前
重构前的代码内容不多,看起来也不是很难以理解,更便于初学者学习。
// 创建存储链式调用的数组 首位是核心调用方法dispatchRequest,第二位是空 var chain = [dispatchRequest, undefined]; // 创建 promise 为什么resolve(config)是因为 请求拦截器最先执行, // 所以设置请求拦截器时可以拿到每次请求的所有config配置 var promise = Promise.resolve(config); // 把设置的请求拦截器的成功处理函数、失败处理函数放到数组最前面 this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { chain.unshift(interceptor.fulfilled, interceptor.rejected); }); // 把设置的响应拦截器的成功处理函数、失败处理函数放到数组最后面 this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { chain.push(interceptor.fulfilled, interceptor.rejected); }); // 循环 每次取两个出来组成promise链.then执行 while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } // 返回promise return promise; 复制代码
promise链是从左到右执行的,先执行请求拦截器,再执行请求,最后执行响应拦截器。
由于拦截器可以有多个(所以用forEach),所以会有顺序问题,由于unshift和push的方法
请求拦截器中后加的拦截器会被放到前面,先执行
响应拦截器中后加的拦截器会被放到后面,后执行
重构后
主要是为了解决请求拦截器中可能会出现异步情况或当前宏任务执行时间过长而阻塞真正请求执行的问题
// 请求拦截器储存数组 var requestInterceptorChain = []; // 默认所有请求拦截器都为同步 var synchronousRequestInterceptors = true; // 遍历注册好的请求拦截器数组 this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { // 这里interceptor是注册的每一个拦截器对象,axios请求拦截器向外暴露了runWhen配置 // 来针对一些需要运行时检测来执行的拦截器 // 如果配置了该函数,并且返回结果为true,则记录到拦截器链中,反之则直接结束该层循环 if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) { return; } // interceptor.synchronous 是对外提供的配置,可标识该拦截器是异步还是同步 默认为false(异步) // 这里是来同步整个执行链的执行方式的,如果有一个请求拦截器为异步, // 那么下面的promise执行链则会有不同的执行方式 synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous; // 塞到请求拦截器数组中 requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); }); // 相应拦截器存储数组 var responseInterceptorChain = []; // 遍历按序push到拦截器存储数组中 this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); }); var promise; // 如果为异步 其实也是默认情况 if (!synchronousRequestInterceptors) { // 这里和重构之前的逻辑是一致的了 var chain = [dispatchRequest, undefined]; // 请求拦截器塞到前面 Array.prototype.unshift.apply(chain, requestInterceptorChain); // 响应拦截器塞到后面 chain = chain.concat(responseInterceptorChain); promise = Promise.resolve(config); // 循环 执行 while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } // 返回promise return promise; } // 这里则是同步的逻辑 var newConfig = config; // 请求拦截器一个一个的走 返回 请求前最新的config while (requestInterceptorChain.length) { var onFulfilled = requestInterceptorChain.shift(); var onRejected = requestInterceptorChain.shift(); // 做异常捕获 有错直接抛出 try { newConfig = onFulfilled(newConfig); } catch (error) { onRejected(error); break; } } // 到这里 微任务不会过早的创建 也就解决了微任务过早创建、 // 当前宏任务过长或某个请求拦截器中有异步任务而阻塞真正的请求延时发起问题 try { promise = dispatchRequest(newConfig); } catch (error) { return Promise.reject(error); } // 响应拦截器执行 while (responseInterceptorChain.length) { promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift()); } return promise; 复制代码
给请求方法设置别名
设置别名的方式更便于axios的使用,在第一篇文章中,我们也列举了别名方式的使用示例。
而在设置别名的时候,由于有些方式是没有data参数的,所以也需要分类处理。
无Data参数:delete、get、head、options
有Data参数:post、put、patch
utils.forEach(['delete', 'get', 'head', 'options'],function forEachMethodNoData(method) { Axios.prototype[method] = function(url, config) { return this.request(mergeConfig(config || {}, { method: method, url: url, data: (config || {}).data })); }; }); utils.forEach(['post', 'put', 'patch'],function forEachMethodWithData(method) { Axios.prototype[method] = function(url, data, config) { return this.request(mergeConfig(config || {}, { method: method, url: url, data: data })); }; }); 复制代码
导出axios
module.exports = Axios;
作者:前端SkyRain
链接:https://juejin.cn/post/7033952469453373447