本文参加了每周一起学习200行源码共读活动
Axios 的核心代码位于 lib/core/Axios
。
首先什么是核心代码?我理解的是通过一定的逻辑,将工程中的各个模块按照一定流程的进行组合暴露给外部使用的,就是核心代码,说白了就是没它不行。
根据第一篇文章中的流程图,我们可以看到,对外暴露的主函数是在 lib/axios
,lib/axios
中主要是 继承 了 lib/core/Axios
的 Axios
。 Axios
内部就是整个 axios 库的核心操作:发起请求的前后处理。
整个 Axios
采用了 ES5
的写法,使用 function
构建了一个类,之后将各个函数挂载在主函数 Axios
的 prototype
上面。具体的函数关系,可以参考下图。
看完图,我们接下来看代码。
Axios 主函数
/**
* Create a new instance of Axios
*
* @param {Object} instanceConfig The default config for the instance
*/
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
主函数很简单,就做了两个事:
- 初始化配置信息
this.defaults
,这个其实就是我们不管是通过axios.create(config)
创建实例后请求,还是直接使用axios(url, config)
还是axios(config)
来请求 ( 包含axios.request()
等 ),或者是使用axios.defaults
来设置默认配置,所用到的config
都和 这个this.defaults
相关,具体的我们一会看。 - 初始化拦截器
this.interceptors
,在这里其实拦截器是request
和response
分别初始化了两个InterceptorManager
实例,InterceptorManager
实例具体是如何实现的,内部又有哪些操作,暂且按下不表,这个是我们下一篇的内容,即使不了解InterceptorManager
也不会影响对整个request
的理解。(稍微剧透下,InterceptorManager
内部的基础数据结构是一个数组)
request 请求主流程
乍一看,整个 request 函数 有点多,但其实它只做了两件事:config 处理及 构建
Promise` 链 ( 包含请求发送 )。
config 处理
/**
* Dispatch a request
*
* @param {Object} config The config specific for this request (merged with this.defaults)
*/
Axios.prototype.request = function request(configOrUrl, config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
// * 此注释为博主自行标注: 第一步
if (typeof configOrUrl === 'string') {
config = config || {};
config.url = configOrUrl;
} else {
config = configOrUrl || {};
}
// * 此注释为博主自行标注: 第二步
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();
} else {
config.method = 'get';
}
// * 此注释为博主自行标注: 第四步
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);
}
...
}
整个代码的前三分之一主要是对 config
进行处理。
第一步是判断到底是按 request(url, config)
还是按 request(config)
的方式传入,从而统一为 request
内部使用的 config
方式,即 url
包含在 config
内部。
第二步是将之前你设置过的也好,axios
默认的也好,都和你此次传入的进行合并,主要用到了 mergeConfig
,关于 mergeConfig
的具体实现,我们会在后续的章节中说到。
第三步是确认使用了什么样的请求方式,可以看到,axios
在这里设置了默认的请求方式 get
。
第四步主要是做了兼容性验证处理,这一块也是以后会有单独的章节。
构建 Promise 链
/**
* Dispatch a request
*
* @param {Object} config The config specific for this request (merged with this.defaults)
*/
Axios.prototype.request = function request(configOrUrl, config) {
...
// 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);
});
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
var promise;
// * 此注释为博主自行标注: 第二步-1
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());
}
return promise;
}
// * 此注释为博主自行标注: 第二步-2
var newConfig = config;
// * 此注释为博主自行标注: 第二步-2.1
while (requestInterceptorChain.length) {
var onFulfilled = requestInterceptorChain.shift();
var onRejected = requestInterceptorChain.shift();
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected(error);
break;
}
}
// * 此注释为博主自行标注: 第二步-2.2
try {
promise = dispatchRequest(newConfig);
} catch (error) {
return Promise.reject(error);
}
// * 此注释为博主自行标注: 第二步-2.3
while (responseInterceptorChain.length) {
promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
}
return promise;
}
第一步是预构建了两条链:请求拦截器链和相应拦截器链。通过调用 InterceptorManager
实例的 forEach
方法来遍历所有设置好的拦截器
第二步是有两个分支,这两个分支主要是根据当时设置拦截器函数时,是否设置了 synchronous
来判断,即 axios.interceptors.request.use(fulfilled,rejected,options)
,中在 options
里设置 synchronous
。
- 如果有一个拦截器的
options
不存在synchronous
,则直接构建一个Promise
链,链上分别是:请求拦截器、请求派发、响应拦截器。 - 如果所有拦截器的
options
中都存在synchronous
, 代表你想要同步执行请求拦截器,则将整个请求拆分为三步执行:同步执行请求拦截器、同步发送请求并使用 promise 包装响应、通过前一步的Promise
响应构建Promise
响应链并执行。
一般情况下,我们走的都是第一个分支,也就是 if (!synchronousRequestInterceptors)
。
在构建 Promise
链的过程中,有几处需要我们注意下:
-
不论请求拦截器还是响应拦截器都是一对一对的。这是为了贴合
promise
,promise.then()
可以放入两个函数,第一个是正常执行后调用的函数,第二个是异常执行后调用的函数。正好和拦截器的分配是一样的,都是一对( 正常 + 异常 )。 -
var chain = [dispatchRequest, undefined]
也是为了凑齐一整对,dispatchRequest
是请求派发的方法,它的入参和请求拦截器是一致的,即config
在经过所有的请求拦截器处理之后,就会送入dispatchRequest
中进行解析并决定使用什么样的方式去派发请求。这也是为什么请求url
最后也要放在config
中的原因。 -
我们在调试 axios 的拦截器时会发现,请求拦截器是按照拦截器加载的倒序执行,响应拦截器是按照拦截器加载的正序执行。这一块我们可以从上面 requestInterceptorChain 数组构建中发现端倪。
requestInterceptorChain
是使用unshift
,也就是每一对拦截器都是直接插入整个requestInterceptorChain
的头部。而responseInterceptorChain
是使用的push
,也就是每一对拦截器都是直接插入整个responseInterceptorChain
的尾部。- 所以,你会发现请求拦截器和响应拦截器的执行顺序正好是相反的。
getUri
Axios.prototype.getUri = function getUri(config) {
config = mergeConfig(this.defaults, config);
var fullPath = buildFullPath(config.baseURL, config.url);
return buildURL(fullPath, config.params, config.paramsSerializer);
};
这个方法就很简单,其实就是将 params
传给 paramsSerializer
进行处理,并对 baseURL
、url
、处理后的 params
进行了字符串拼接。
常用的请求 method 方法
// 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
}));
};
});
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
}));
};
});
我们在日常使用中,不仅仅可以直接通过 axios()
的方式发送请求,也可以通过 axios.get()
这种方式发送请求。
像是 get
、put
这种别名方法就是通过上面的代码实现的。
挂载时它分成了两种挂载方式:
- 不可携带 data 的:从
config
中获取data
- 可以携带 data 的:设置第二个传参就是
data
。
当然这里的不可携带 data
,并不是真的不给你携带,只是可以传,但没必要。
到此,整个 Axios
核心代码就基本了解完了。当然我们常用的还有 cancel
和 axios.all
等相关方法并不在核心代码中,这些我们都会在后续的文章中一一拆解。(话说 axios.all
感觉都没必要去分析,它就是个 Promise.all
啊。。。)