阅读axios源码,可解释下列问题:
1.为什么axios既可以像函数一样调用,也可以使用别名,如axios.request、axios.post
这种方式来发起请求呢?
2.创建axios的过程中发生了什么
3.interceptors拦截器是如何实现的
4.CancelToken是如何实现的
5.为什么axios在浏览器环境和node环境里都能被调用
之前有看vue源码的计划,太多了,看着发困,遂弃。
找个代码量没那么多的axios循序渐进。
受若川视野文章启发,出此文记之
关于调试:调试的方法文章里面写得很清楚了。因为axios支持node调用,所以调试的时候,启动一个server 和一个client ,通过node代码调用axios的get和post方法发送请求,打断点一步步看执行的情况。
一、创建实例
客户端使用var axios = require('../index');
得到axios实例发生了什么
首先进入的是lib/axios.js
里面进行axios实例的初始化,关键代码如下。
// lib/axios.js
var axios = createInstance(defaults);
axios.Axios = Axios;
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
可以带着第一个问题来看这个过程:为什么axios既可以像函数一样调用,也可以使用别名,如axios.request、axios.post
这种方式来发起请求呢?
逐一分析:
1.defaults
创建实例传入的defaults,里面是对header、adapter、xsrfCookieName、xsrfHeaderName属性的赋值。特别地,初始化了headers的一些方法,以便使用时可以通过调用axios.defaults.headers的方式去查询、修改到默认的headers赋值
/* lib/defaults */
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
defaults.headers[method] = {};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});
对照axios文档,使用场景是这样的
// 配置的默认值/defaults
// 你可以指定将被用在各个请求的配置默认值
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
可以在使用时先设定好默认使用的参数,如果不主动设置将走默认配置
2.创建实例 createInstance(defaults)
/* lib/axios.js */
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context);
// Copy context to instance
utils.extend(instance, context);
return instance;
}
这个函数解释了最初提出的问题,关于axios的调用方式灵活性。
- 首先使用
new Axios
的方式去得到context这个对象。对象有两个属性:上文说的defaults、和拦截器interceptors。context将作为上下文,在之后不断被使用。
/* lib/core/Axios.js */
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
- 得到instance方法。
var instance = bind(Axios.prototype.request, context);
bind 这个方法很有意思,通过闭包的形式,实现传进参数后,执行以context为上下文的request方法。
// bind.js
module.exports = function bind(fn, thisArg) {
return function wrap() {
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return fn.apply(thisArg, args);
};
};
- 接下来是
utils.extend(instance, Axios.prototype, context);
目的是 遍历Axios.prototype的属性,逐一给instance赋值,如果该属性为方法,则以context对象为上下文。
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;
}
这里Axios.prototype的属性有:request、getUri还有别名'delete', 'get', 'head', 'options','post', 'put', 'patch'
别名们通过给request方法入参传入method属性来实现调用,本质上还是调用了request。所以axios({method:'get'})
同 axios.get
是一样的。
/* lib/core/Axios.js */
// 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(utils.merge(config || {}, {
method: method,
url: url
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, data, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data
}));
};
});
- 最后是把context里的属性和方法赋值给instance
utils.extend(instance, context);
剩下的几步是一些赋值操作,比如允许继承的Axios,让用户自定义传入参数的create方法,用于取消请求操作的Cancel等,还有一个等同于Promise.all 的 axios.all方法
至此实例axios创建完成。
二、发送请求
上文提到,不论是将axios当做方法还是使用别名发起请求,最终调用的还是request方法,代码如下
/* lib/core/Axios.js */
Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
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';
}
// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
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);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
这个方法里的this,就是上文提到的context,内容如下
下面按顺序来分析,当调用了request时,做了些什么
- method
在这里首先判断了method是什么。如果传入了则使用传入的,也支持用户在axios.defaults.method设置默认的request方法,如果都没有读到,则默认的方法为get。 - interceptors 拦截器
interceptors是axios里比较常用的功能,用途为
在请求或响应被 then 或 catch 处理前拦截它们
在发送请求之前,我设置了
axios.interceptors.request.use(
function requestSuccess(config) {
// 在发送请求之前做些什么
console.log('interceptors request');
return config;
},
function requestEnd(error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);
axios.interceptors.response.use(
function responseSuccess(config) {
// 在response回来之前做些什么
console.log('interceptors response');
return config;
},
function responseEnd(error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);
客户端通过发送请求之前,调用use方法对InterceptorManager维护的handlers数组进行添加操作。
而在request
方法里,对于拦截器的处理方法是——维护一个名为chain的成功失败回调数组。request 的interceptors方法在chain的头部插入,response的interceptors方法在chain的尾部插入
笔者设置了interceptors.request
和interceptors.response
,可以看到此时的chain经处理后如下图
然后循环将这些用promise then 串起来,
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
所以chain是成功失败回调一前一后排列的顺序
- dispatchRequest
在请求拦截器设置的回调执行完成之后,将会执行dispatchRequest,代码如下
/* lib/core/dispatchRequest */
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
// Ensure headers exist
config.headers = config.headers || {};
// Transform request data
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
// Flatten headers
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];
}
);
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data
response.data = transformData(
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(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
};
这个方法先是对入参config进行处理。比如在transformRequest
里根据data的类型设置headers,删掉之前对headers添加的方法
最后执行adapter方法去发送请求。
- adapter
getDefaultAdapter 这个方法判断当前环境是node还是浏览器,选择对应的发送请求的方法。解释了为什么axios支持两个环境。
/* lib/defaults.js*/
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;
}
- CancelToken 实现原理
简单说来是在请求的过程中,通过执行XMLHttpRequest.abort()
去中断当前请求。
看之前有一个疑问,既然在请求时已经执行完xhr.js里的代码了,而这个cancelToken是在xhr.js里的。那要如何实现在请求发送之后,客户端可以调用CancelToken去中断这个请求的执行呢?
// 客户端代码
var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
// lib/cancel/CancelToken.js
'use strict';
var Cancel = require('./Cancel');
/**
* A `CancelToken` is an object that can be used to request cancellation of an operation.
*
* @class
* @param {Function} executor The executor function.
*/
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
/**
* Throws a `Cancel` if cancellation has been requested.
*/
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
/**
* Returns an object that contains a new `CancelToken` and a function that, when called,
* cancels the `CancelToken`.
*/
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
module.exports = CancelToken;
// lib/adapters/xhr.js
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
从客户端发起请求说起。
CancelToken.source()
执行之后,将得到一个对象{token:token,cancel:cancel}
token是CancelToken实例 ,这个实例拥有一个名为promise的属性;cancel是一个方法,用闭包的方式保存了创建实例时CancelToken的上下文。
让我们再仔细看CancelToken里的实现
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
这个位于CancelToekn里的cancel就是实例里的cancel。如果执行它,将会执行resolvePromise,即触发this.promise的成功回调
又因为在发送请求时,xhr里做了这样的处理
// lib/adapters/xhr.js
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
所以当config.cancelToken.promise
的成功回调被触发,将会走到then的成功回调onCanceled
里,request.abort()
执行之后就可以达到中断当前请求的目的。