版本说明
此博客主要是针对 axios
@0.23.0做一次源码解析,主要研究以下几点:
axios
的初始化过程- 核心请求方法
Axios.prototype.request
的实现 - 取消相关的
API
的实现。
思维导图
一、初始化过程
从git
上down
下源码并install
后,打开package.json
文件,main
字段声明了主入口文件的位置:
// package.json
{
"name": "axios",
"version": "0.23.0",
"description": "Promise based HTTP client for the browser and node.js",
"main": "index.js",
"types": "index.d.ts",
...
}
从中可以看到主入口文件是index.js
,打开index.js
文件,里面只有一段代码,加载了lib
文件夹下axios.js
文件:
module.exports = require('./lib/axios');
所以真正的源码文件是axios.js
,可以看到该文件通过调用createInstance
函数并传入defaults
变量,初始化了 axios
变量:
// Create the default instance to be exported
var axios = createInstance(defaults);
我们平时导入的axios
的构造函数其实就是createInstance
函数,我们先看这一构造函数:
function createInstance(defaultConfig) {
// new 一个 Axios 生成实例对象,私有属性包含 defaults(参数)和 interceptors(拦截器)
var context = new Axios(defaultConfig);
// bind 返回一个新的 wrap 函数
// 调用 axios 的本质其实是调用 Axios.prototype.request
var instance = bind(Axios.prototype.request, context);
// 复制 axios.prototype 到 instace 函数(wrap)上
// 也就是为什么会有 axios.get 等别名方法
// 且调用的是 Axios.prototype.get 方法
utils.extend(instance, Axios.prototype, context);
// 复制 axios.prototype 到 instace 函数(wrap)上
// 也就是为什么默认配置 axios.defaults 和拦截器 axios.interceptors 可以使用的原因
// 其实就是 new Axios().defaults 和 new Axios().interceptors
utils.extend(instance, context);
// 工厂模式 创建新的实例 用户可以自定义一些参数
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
return instance;
}
1. Axios
构造函数
可以看到上面构造函数第一句就是实例化Axios
实例,这一构造函数分三部分进行叙述。Axios
构造函数源码位于./core/Axios.js中:
- 第一部分:初始化两个属性,一个默认参数
defaults
,另一个则是interceptors
拦截器。- 第二部分:
Axios.prototype.request
方法,该方法是所有请求真正调用的方法。- 第三部分:其他请求方法,比如:
delete
,get
,post
等。
1.1 初始化属性
function Axios(instanceConfig) {
// 默认参数
this.defaults = instanceConfig;
// 拦截器 请求和响应的拦截器
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
1.1.1 defaults
默认参数
默认参数其实是在初始化axios
时传入的,其引用的是./lib/defaults导出的defaults
变量:
var defaults = {
transitional: {
silentJSONParsing: true,
forcedJSONParsing: true,
clarifyTimeoutError: false
},
// 默认适配器方法
adapter: getDefaultAdapter(),
// 请求数据转换默认方法
transformRequest: [function transformRequest(data, headers) {
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
if (utils.isFormData(data) ||
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {
setContentTypeIfUnset(headers, 'application/json');
return stringifySafely(data);
}
return data;
}],
// 响应数据转换默认方法
transformResponse: [function transformResponse(data) {
var transitional = this.transitional || defaults.transitional;
var silentJSONParsing = transitional && transitional.silentJSONParsing;
var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
var strictJSONParsing = !silentJSONParsing && this.responseType === 'json';
if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) {
try {
return JSON.parse(data);
} catch (e) {
if (strictJSONParsing) {
if (e.name === 'SyntaxError') {
throw enhanceError(e, this, 'E_JSON_PARSE');
}
throw e;
}
}
}
return data;
}],
/**
* A timeout in milliseconds to abort a request. If set to 0 (default) a
* timeout is not created.
*/
// 默认延时器
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
// 校验状态码
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
},
// 请求头
headers: {
common: {
'Accept': 'application/json, text/plain, */*'
}
}
};
module.exports = defaults;
可以看到里面是常用的默认配置,比如:请求头headers
,请求数据转换默认方法transformRequest
,适配器方法adapter
等。这些默认配置可以通过axios.defaults.xx
来改变(初始化过程会将实例化的Axios
挂载到axios
上,这些下面会讲)。
1.1.2 interceptors
拦截器
可以看到interceptors
是一个对象,里面有两个属性request
和response
,这两个属性的值都是实例化了一个InterceptorManager
变量。该构造函数在core/InterceptorManager:
var utils = require('./../utils');
// 初始化
function InterceptorManager() {
this.handlers = [];
}
// use 方法:添加一个拦截器放入栈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;
}
// eject方法:从栈中移除拦截器
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
}
// forEach: 迭代所有拦截器,并执行
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
}
上面的代码很简单,就是定义了一个构造函数InterceptorManager
,里面有一个私有属性handlers
用于存放用户通过use
方法创建的拦截器。eject
方法用于删除handlers
存在的拦截器,forEach
方法遍历所有拦截器,并执行。
1.2 Axios.prototype.request
方法
该方法是整个库中的核心方法,想单独拿出来讲,这里先不做阐述。
1.3 其他请求方法
// 提供一些请求方法的别名
// 遍历执行
// 这也就是我们可以用 axios.get 等别名方式的调用,实际调用的是 Axios.prototype.request 方法
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
}));
};
});
想看懂上面的代码,首先得了解两个工具函数utils.forEach
和mergeConfig
,从命名上就可以看出,一个是遍历,一个是合并配置。具体是不是呢?可以看源码:
function forEach(obj, fn) {
// Don't bother if no value provided
if (obj === null || typeof obj === 'undefined') {
return;
}
// Force an array if not already something iterable
if (typeof obj !== 'object') {
/*eslint no-param-reassign:0*/
obj = [obj];
}
if (isArray(obj)) {
// Iterate over array values
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
// Iterate over object keys
// 用 for in 遍历对象,但 for in 会遍历原型链上可遍历的属性
// 所以用 hasOwnProperty 来过滤自身属性
// 其实也可以用 Object.keys 来遍历,它不遍历原型链上可遍历的属性
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
}
}
module.exports = {
forEach: forEach
}
可以清除的看到utils.forEach
就是遍历传入的obj
来执行fn
方法,类似数组中的forEach
方法。
module.exports = function mergeConfig(config1, config2) {
// eslint-disable-next-line no-param-reassign
config2 = config2 || {};
var config = {};
...
// 根据两个配置项的键 key 来遍历,取到 mergeMap[key] 的值
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;
};
mergeConfig
方法很清晰:合并传入的两个配置,如果存在相同的配置,第二个参数中的配置覆盖第一个参数的。
看完这两个工具函数,上面Axios
原型上定义的方法的逻辑也就清楚了,遍历post
,get
等请求字段名,并在Axios
原型上创建与之对应的方法,这也就是为什么axios.get
等别名方法真正调用的方法,其函数内部实际是调用request
方法。
2. 生成instance
实例
// bind 返回一个新的 wrap 函数
// 调用 axios 的本质其实是调用 Axios.prototype.request
var instance = bind(Axios.prototype.request, context);
// 返回一个新的函数 wrap
module.exports = function bind(fn, thisArg) {
return function wrap() {
// 把 arguments 对象里的数据放到数组 args 里
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
// 调用 fn 函数,上下文环境在 thisArg 里,参数是 args
return fn.apply(thisArg, args);
};
};
通过bind
函数返回一个wrap
函数,wrap
函数内部其实主要就是通过apply
方法改变this
指向,将request
方法的作用域挂载到Axios
实例上,就可以访问它的属性和方法。
3. 别名方法的实现
// 复制 Axios.prototype 到 instace 函数(wrap)上
// 也就是为什么会有 axios.get 等别名方法
// 且调用的是 Axios.prototype.get 方法
utils.extend(instance, Axios.prototype, context);
// 遍历 b 对象上的属性,将其放到 a 对象上,如果是函数则用 bind 调用
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;
}
forEach
方法在上面已经讲过了,该步骤是通过遍历Axios.prototype
对象上的属性和方法,并复制到instance
(wrap)函数上,这也就是为什么能有axios.get
等别名方法的原因。extend
工具函数会判断传入的值是不是函数,如果是函数则会用bind
绑定该函数的上下文环境,别名方法的上下文环境在Axios
实例上,也就可以访问Axios
的属性和方法。
4. 扩展axios
属性
// 复制 axios.prototype 到 instace 函数(wrap)上
// 也就是为什么默认配置 axios.defaults 和拦截器 axios.interceptors 可以使用的原因
// 其实就是 new Axios().defaults 和 new Axios().interceptors
utils.extend(instance, context);
extend
工具方法上面刚讲过,这句代码就是将context(new Axios)
的属性和方法复制到instance
方法上,也就是说instance
方法上有Axios
的属性和方法。
5. axios.create
方法
// 工厂模式 创建新的实例 用户可以自定义一些参数
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
工厂模式,在instance
函数上,创建一个create
方法,该方法其实是调用createInstance
方法,用户可以自定义一些参数。
6. 返回 intance
实例
return instance;
二、Axios.prototype.request
实现
在初始化过程中遗留了一点没有讲到,就是所有请求真正调用的方法Axios.prototype.request
,这一部分我们逐层分析该方法的实现,该源码位于core/Axios.js:
1. 解析config
// Allow for axios('example/url'[, config]) a la fetch API
// 这一段代码 其实就是使用 axios('example/url') config 可以省略
// 最终都是放到 config 里面
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
// 合并默认参数和用户传递的参数
config = mergeConfig(this.defaults, config);
// Set config.method
// 设置请求的方法,默认是 get
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);
}
该段代码做了如下几件事:
- 判断
config
是否是字符串,如果是字符串则看有没有传入第二个参数,没传的话默认是空对象,并将第一个参数赋值给url
属性,确保config
是对象。(这一段代码 其实就是可以使用axios('example/url')
的原因); - 通过
mergeConfig
方法合并传入的配置与默认配置,如果存在相同的配置项,传入的配置会覆盖; - 定义
config.method
,确保是小写字母,并且默认是get
请求; - 异常检测
config.transitional
,防止用户传入的格式不正确。
2. 拦截器的处理
回顾一下,拦截器是通过use
方法以对象的形式存入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;
}
然后再看这段代码:
var requestInterceptorChain = [];
var synchronousRequestInterceptors = true;
// 遍历用户设置的请求拦截器,放到数组 requestInterceptorChain 前面
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 正在运行直接返回
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
// 同步请求放到 synchronousRequestInterceptors
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
// 将用户请求的函数,放到 requestInterceptorChain 数组中
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
var responseInterceptorChain = [];
// 遍历用户设置的响应拦截器,放到数组 responseInterceptorChain 后面
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
上述代码主要做了两件事,一是,遍历用户设置的请求拦截器,通过unshift
方法插入数组 requestInterceptorChain
前面;二是,遍历用户设置的响应拦截器,通过push
方法放到数组 responseInterceptorChain
后面。
3. 处理异步请求
var promise;
// 不是同步请求
if (!synchronousRequestInterceptors) {
// 组成 `promise`链
// 将 xhr 请求的 dispatchRequest 和 undefined 放到同一数组中
var chain = [dispatchRequest, undefined];
// 将用户设置的请求拦截器放到 chain 最前面
Array.prototype.unshift.apply(chain, requestInterceptorChain);
// 将用户设置的响应拦截器放到 chain 后面
chain = chain.concat(responseInterceptorChain);
// 创建 promise 实例
promise = Promise.resolve(config);
// 遍历 chain 数组
while (chain.length) {
// 两两对应移出来,放到 then 的两个参数里
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
如果用户传入的拦截器配置synchronous
属性为true
则说明是同步,如果没有传入或者传入的是false
,则走这段异步请求,默认的是异步请求。这段代码做了如下四件事:
- 将
xhr
请求的dispatchRequest
和undefined
放到同一数组中,为什么要放个undefined
进去?请看第三步。 - 将用户设置的请求拦截器放到
chain
最前面,将用户设置的响应拦截器放到 chain 后面; - 创建
promise
实例,遍历chain
,两两对应移出来调用,放到微任务队列里; - 返回
promise
,做链式调用。
总体来说就是,先去处理拦截器的请求的函数,然后处理dispatchRequest
函数,最后处理拦截器响应的函数。因为拦截器的请求和响应函数是用户自定义的,所以得自己研究,dispatchRequest
函数是源码自带的,我们可以看看该函数具体做了什么。
4. dispatchRequest
函数
因为同步请求和异步请求都有该函数所以放到一层来讲,不单独在异步请求下讲,这一函数将分为三部分阐述,该源码位于core/dispatchRequest.js。
第一部分:处理请求的配置,例如:处理
config.headers
,转换config.data
里的数据,删除请求头里的请求方法;第二部分:适配器
adapter
函数;第三部分:服务器交互成功与失败的处理。
4.1 处理请求的配置
// 如果取消被请求,则抛出一个取消
throwIfCancellationRequested(config);
// 确保 headers 存在
config.headers = config.headers || {};
// 转换请求的数据
config.data = transformData.call(
config,
config.data,
config.headers,
config.transformRequest
);
// 拍平 headers
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers
);
// 删除 headers 里面的这些方法
utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);
上述代码做了如下几件事:
- 如果取消被请求,则抛出一个取消;
- 转换请求的数据,拍平
headers
; - 删除
headers
里的请求方法。
我们主要看下请求转换数据的方法transformData
和headers
拍平的方法,先来看transformData
方法:
module.exports = function transformData(data, headers, fns) {
var context = this || defaults;
/*eslint no-param-reassign:0*/
utils.forEach(fns, function transform(fn) {
data = fn.call(context, data, headers);
});
return data;
};
可以看到该方法是遍历传入的fns
参数,然后执行里面的每个方法。fns
就是上面传入的config.transformRequest
,transformRequest
是可以自己定义的,也可以使用默认配置里面的defaults
用来处理请求头的数据。另一是工具函数merge
:
/**
* 接受对象,合并属性
* Example:
*
* ```js
* var result = merge({foo: 123}, {foo: 456});
* console.log(result.foo); // outputs 456
* ```
*/
function merge(/* obj1, obj2, obj3, ... */) {
var result = {};
function assignValue(val, key) {
if (isPlainObject(result[key]) && isPlainObject(val)) {
result[key] = merge(result[key], val);
} else if (isPlainObject(val)) {
result[key] = merge({}, val);
} else if (isArray(val)) {
result[key] = val.slice();
} else {
result[key] = val;
}
}
for (var i = 0, l = arguments.length; i < l; i++) {
forEach(arguments[i], assignValue);
}
return result;
}
该方法通过遍历传进来的参数,执行assignValue
方法,将每个对象上的属性放到result
中,并返回。也就是说假如,后面对象与前面对象有相同的属性,后面对象的属性会覆盖前面对象的属性。
4.2 adapter
函数
// 适配器
var adapter = config.adapter || defaults.adapter;
可以根据规定自定义适配器方法,也可以使用默认配置中的适配器方法,在default.js
中,分别处理了浏览器环境和node
环境的适配器方法,我们主要看浏览器环境的适配器方法:
/**
* 判断是浏览器环境还是 node 环境
* 1. 浏览器环境会用 xhr 生成适配器
* 2. node 环境生成适配器的方法,暂时还未看
*/
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}
可以看到浏览器适配器方法是在./adapters/xhr.js中,具体可以看看做了什么:
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// 获取基础配置
var requestData = config.data;
var requestHeaders = config.headers;
var responseType = config.responseType;
var onCanceled;
function done() {
if (config.cancelToken) {
config.cancelToken.unsubscribe(onCanceled);
}
if (config.signal) {
config.signal.removeEventListener('abort', onCanceled);
}
}
// 如果传递进来的 data 是表单数据,则删除请求头 Content-Type 属性
if (utils.isFormData(requestData)) {
delete requestHeaders['Content-Type']; // Let the browser set it
}
// XHR 对象用于与服务器交互
var request = new XMLHttpRequest();
// HTTP basic authentication
// HTTP 基本认证
if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
var fullPath = buildFullPath(config.baseURL, config.url);
// 初始化一个请求
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// Set the request timeout in MS
request.timeout = config.timeout;
function onloadend() {
if (!request) {
return;
}
// Prepare the response
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
var responseData = !responseType || responseType === 'text' || responseType === 'json' ?
request.responseText : request.response;
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
};
settle(function _resolve(value) {
resolve(value);
done();
}, function _reject(err) {
reject(err);
done();
}, response);
// Clean up request
request = null;
}
if ('onloadend' in request) {
// Use onloadend if available
request.onloadend = onloadend;
} else {
// Listen for ready state to emulate onloadend
// 监听 state 改变
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// The request errored out and we didn't get a response, this will be
// handled by onerror instead
// With one exception: request that using file: protocol, most browsers
// will return status as 0 even though it's a successful request
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
// readystate handler is calling before onerror or ontimeout handlers,
// so we should call onloadend on the next 'tick'
setTimeout(onloadend);
};
}
// Handle browser request cancellation (as opposed to a manual cancellation)
// 取消
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(createError('Request aborted', config, 'ECONNABORTED', request));
// Clean up request
request = null;
};
// Handle low level network errors
// 错误
request.onerror = function handleError() {
// Real errors are hidden from us by the browser
// onerror should only fire if it's a network error
reject(createError('Network Error', config, null, request));
// Clean up request
request = null;
};
// Handle timeout
// 超时
request.ontimeout = function handleTimeout() {
var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
var transitional = config.transitional || defaults.transitional;
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(createError(
timeoutErrorMessage,
config,
transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',
request));
// Clean up request
request = null;
};
// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (utils.isStandardBrowserEnv()) {
// Add xsrf header
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
// Add headers to the request
if ('setRequestHeader' in request) {
utils.forEach(requestHeaders, function setRequestHeader(val, key) {
if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
// Remove Content-Type if data is undefined
delete requestHeaders[key];
} else {
// Otherwise add header to the request
request.setRequestHeader(key, val);
}
});
}
// Add withCredentials to request if needed
// 跨域时携带 cookies
// 一个 布尔值,用来指定跨域 Access-Control 请求是否应当带有授权信息,如 cookie 或授权 header 头
if (!utils.isUndefined(config.withCredentials)) {
request.withCredentials = !!config.withCredentials;
}
// Add responseType to request if needed
if (responseType && responseType !== 'json') {
request.responseType = config.responseType;
}
// Handle progress if needed
// 上传下载进度相关
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}
// Not all browsers support upload events
// 区别不支持上传事件的浏览器
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}
// 取消的条件
if (config.cancelToken || config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = function(cancel) {
if (!request) {
return;
}
reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
request.abort();
request = null;
};
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
if (!requestData) {
requestData = null;
}
// Send the request
// 发送请求
request.send(requestData);
});
};
可以看到该方法还是挺长的,但是骚年别放弃啊,我们可以大体看看做了什么,总结如下:
- 返回了一个
promise
实例,可以链式调用; - 处理
config
; - 实例化了一个
XHR
对象,用于和服务器交互,后面就是XHR
对象的基本操作,不了解的可以看看MDN
的介绍; - 取消的处理(放到取消部分再讲)。
4.3 服务器交互成功与失败的处理
return adapter(config).then(function onAdapterResolution(response) {
// 有可能是执行到这里被取消,取消的情况执行这句
throwIfCancellationRequested(config);
// Transform response data
// 转换响应的数据
response.data = transformData.call(
config,
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
// 取消相关
// 也有可能是执行到这句才取消,取消的情况下执行这句
throwIfCancellationRequested(config);
// Transform response data
// 转换响应的数据
if (reason && reason.response) {
reason.response.data = transformData.call(
config,
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
成功和失败都会执行config.transformResponse
的函数,成功会将处理后的数据放在response.data
中,失败则会走Promise.reject(reason)
,会走链式里的reject
方法。这个中间处理方法也大体讲完了,里面细节需要自己去调试去看。
5. 同步请求处理
// 从 requestInterceptorChain 数组中,取出每个请求,并依次执行
var newConfig = config;
while (requestInterceptorChain.length) {
var onFulfilled = requestInterceptorChain.shift();
var onRejected = requestInterceptorChain.shift();
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected(error);
break;
}
}
// 调用 dispatchRequest 方法
try {
promise = dispatchRequest(newConfig);
} catch (error) {
return Promise.reject(error);
}
// 走响应成功的方法
while (responseInterceptorChain.length) {
promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
}
// 返回 promise 实例用于链式调用
return promise;
很简单,流程就是:处理所有拦截器request
方法 -> dispatchRequest
与服务器交互 -> 拦截器response
方法。
三、取消相关API
的实现
在./lib/axios.js
中初始化axios
默认实例后,又给axios
函数挂载了一些属性和方法:
xios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
axios.VERSION = require('./env/data').version;
// Expose all/spread
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
// Expose isAxiosError
axios.isAxiosError = require('./helpers/isAxiosError');
module.exports = axios;
我们主要看这个axios.CancelToken
取消令牌函数,这个函数可以取消请求,具体用法如下:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
source.cancel('取消操作~');
可以看到使用了构造函数的静态方法source
,该方法在./cancel/CancelToken.js:
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
token
就是CancelToken
的实例,可以看看实例化时都做了什么:
function CancelToken(executor) {
// 类型判断,传入的执行器必须是函数
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
// 定义了 this.promise,并将 resolve 赋给全局变量 resolvePromise
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
// 存入微任务队列
this.promise.then(function(cancel) {
if (!token._listeners) return;
var i;
var l = token._listeners.length;
for (i = 0; i < l; i++) {
token._listeners[i](cancel);
}
token._listeners = null;
});
// then 重新赋值
this.promise.then = function(onfulfilled) {
var _resolve;
// eslint-disable-next-line func-names
var promise = new Promise(function(resolve) {
token.subscribe(resolve);
_resolve = resolve;
}).then(onfulfilled);
promise.cancel = function reject() {
token.unsubscribe(_resolve);
};
return promise;
};
// 执行执行器方法
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
主要做了如下几件事:
- 类型判断,传入的执行器必须是函数;
- 定义了
this.promise
,并将 resolve 赋给全局变量resolvePromise
; then
里面执行token._listeners
里面的所有函数。then
重新赋值;- 执行执行器方法,将
cancel
方法赋值给source
里面的cancel
属性。
可以看到then
里面有个token._listeners
属性,这个属性在哪生成呢?是在适配器那部分代码中有一段:
// 取消的条件
if (config.cancelToken || config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = function(cancel) {
if (!request) {
return;
}
reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
request.abort();
request = null;
};
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
根据传入的配置是否存在cancelToken
属性,来调用subscribe
方法,而subscribe
方法就是生成_listeners
属性的。
CancelToken.prototype.subscribe = function subscribe(listener) {
if (this.reason) {
listener(this.reason);
return;
}
if (this._listeners) {
this._listeners.push(listener);
} else {
this._listeners = [listener];
}
};
也就是说,当执行then
里面微任务时,会执行onCanceled
方法,该方法中又有一句request.abort()
,则会触发request
的onabort
事件,取消请求。
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(createError('Request aborted', config, 'ECONNABORTED', request));
// Clean up request
request = null;
};
到这,整个取消请求的流程已经讲解完了,细节还是需要自己去看~
欢迎提错,共同学习~~