Axios是一个很常用的http请求库,功能强大,应用也很灵活,可用于node和浏览器环境。也可以扩展自己的请求方法。这么好用强大的工具,原理是不是很复杂呢?
NO NO NO,并不复杂
1、request方法,Axios外部方法其实都是在调用这一个方法
2、方法内部创建一个Promise链式调用,常用的功能,拦截器,数据修改器,http请求,就是在这个Promise链式调用中逐步被执行。request方法返回Promise链。我们用的就是这个返回的Promise,执行结果就在这个Promise中的状态值中。
如果您对Promise还不是很熟悉,建议先学习之后再研究axios的原理。
深入分析原理之前,先了解一下Axios库中都提供了什么功能,根据这些功能。我们在看这个功能是怎么实现的,进而就可以了解Axios实现的原理了。
一、 Axios提供的功能
- http请求用来发送http请求的方法。例如axios(config)
- 并发请求用来同时处理多个axios请求axios.all()
- 拦截器(interceptors)请求拦截器(interceptors.request)是指可以拦截住每次或指定http请求,并可修改配置项响应拦截器(interceptors.response)可以在每次http请求后拦截住每次或指定http请求,并可修改返回结果项。
- 数据修改器(transformRequest、transformResponse)请求转换器(transformRequest)是指在请求前对数据进行转换,响应转换器(transformResponse)主要对请求响应后的响应体做数据转换。
- 取消请求通过config中的CancelToken属性,控制取消axios
二、Axios项目结构

上面的目录结构中的文件没有完全列出来。这些是整个流程中的核心,其他的功能性的代码。在理解了基本原理之后,大家去看其他代码就非常容易了。
三、基本内部工具方法介绍
三、merge:深度合并多个对象
四、extend:将一个对象的方法和属性扩展到另外一个对象上,并指定this
五、mergeConfig:深度合并config的方法
接下来开始分析Axios原理:
四、多种形式调用Axios的原理
// 第一种方法
axios(config)
// 第二种
axios('example/url'[, config])
// 第三种
axios.request(config)
// 第四种
axios.get(url[, config])//'delete', 'get', 'head', 'options'请求方法一样的调用方式
// 第五种
axios.post(url[, data[, config]])// 'post', 'put', 'patch'请求方法永阳的调用方式
// 第六种
axios.all([axios1, axios2, axios3]).then(axios.spread(function (axios1response, axios2response, axios3response) {
// 三个请求现在都执行完成
}));
// 还可以通过axios.create方法建立自定义全局默认配置的Axios实例
axios.create(config)总结有六种调用方式
如何实现的呢?来看看axios设置对外接口的源码lib / axios.js,把主要的代码复制出来
function createInstance(defaultConfig) {
// 建立Axios对象
var context = new Axios(defaultConfig);
// Axios作者的目的是提供一个对外可用的方法。
// 并且方法中需要用到Axios对象中的config属性和拦截器。
// 所以要把axios原型上的方法单独拿出来,绑定context这个axios实例。
// instance方法就是后面导出的axios实例,
// 所以到这里位置 第一种调用方法 axios(config) 就实现了
// 在request方法的内部,有对传入参数类型的判断,如果传入第一参数为字符串,则认为是url字符串,并且把url参数添加到第二个参数config中
// 所以就实现了第二种调用方法axios('example/url'[, config])
var instance = bind(Axios.prototype.request, context);
// 这里把Axios原型上的方法和属性,扩展到instance方法上,
// 并制定了原型上方法的this为context(上面定义axios对象)
// Axios上有request方法,这里绑定了this为context
// 所有第三种调用方法 axios.request(config) 就实现了
// Axios原型中其实定义了get,post,option等等方法,
// 所以第四种axios.get(url[, config])和第五种axios.post(url[, data[, config]])方法就实现了
utils.extend(instance, Axios.prototype, context);
// 这里把上面建立axios对象(context)中自有的属性方法,扩展到了instance中
// 这样instance就有了defaults、interceptors 属性,就可以添加拦截器,操作defaultConfig了
utils.extend(instance, context);
return instance;
}
// 调用createInstance方法,建立了Axios实例
var axios = createInstance(defaults);
// 这里也调用上面的createInstance方法,同样建立了Axios实例,
// 只不过,这里配置了自己的config作为全局默认的config
// 所以这里实现了,通过axios.create方法建立自定义默认配置的Axios实例
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// 这里添加了all方法,其实就是promise的all方法,
// 这就是第六种调用方法,并发请求的实现原理
axios.all = function all(promises) {
return Promise.all(promises);
};
// spread方法就是把用数组作为一个参数,变成数组的每一项为一个参数。就是为了用着方便。
axios.spread = require('./helpers/spread');
module.exports = axios;// 对外导出实例
上面注释中提到的第二种调用方法axios('example/url'[, config])的实现是request内部做了判断所以才能那么用。第四种axios.get(url[, config])和第五种axios.post(url[, data[, config]])方法的实现是因为Axios原型中定义了相应的方法,所以才得以实现。那么我们就看看源码是怎么写的吧,这里打开lib/core/Axios.js
// Axios构造函数,定义l额defaults属性和interceptors属性
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
Axios.prototype.request = function request(config) {
// 这里对传入参数类型的判断,如果传入第一参数为字符串,
// 则认为字符串是url,并且把url参数添加到第二个参数config中
// 所以就实现了第二种调用方法axios('example/url'[, config])
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
// ...省略了一些代码
}
// 这里对Axios原型扩展了'delete', 'get', 'head', 'options'方法,
// 其实都是调用了request方法
// 结合上面lib / axios.js 代码中把原型中方法扩展到了instance上
// 所以第四种方法axios.get(url[, config])就实现了
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
}));
};
});
// 这里对Axios原型扩展了'post', 'put', 'patch'方法,
// 其实都是调用了request方法
// 结合上面lib / axios.js 代码中把原型中方法扩展到了instance上
// 所以第五种方法axios.post(url[, 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
}));
};
});五、注册拦截器
// 添加请求拦截器
const myRequestInterceptor = axios.interceptors.request.use(config => {
// 在发送http请求之前对config做点什么
return config; // 必须返回config否则后续http请求无法获取到config
}, error => {
// 错误处理代码写在这里
// 但是,为什么返回Promise.reject(error)
// 这和promise的机制有关
// 如果直接抛出error就相当于抛出一个对象,
// 这就会运行下一级promise的fulfilled方法。并且参数是error,这个fulfilled参数应该是config才对
// 或者运行到dispatchRequest,参数error,但dispatchRequest参数也要是config才对
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(response => {
// 对响应数据处理代码写这里
return response; // 有且必须有一个response对象被返回
}, error => {
// 对响应错误处理代码写这里
// 同理请求拦截器,这需要返回Promise.reject(error);
return Promise.reject(error);
});
// 移除某次拦截器
axios.interceptors.request.eject(myRequestInterceptor);前面提到过在lib / axios.js这里已经将context对象的属性扩展到了instance,也就是对外接口axios。所以axios就有了interceptor属性,里面有两个属性request和response,都是InterceptorManager对象,目的是存储注册的拦截器。
那么InterceptorManager都做了什么呢,看源码:
// 构造函数,在对象中定义handlers用来存储拦截器
function InterceptorManager() {
this.handlers = [];
}
/**
* 注册拦截器,并存储在handlers中
* 参数fulfilled,用来拦截器处理数据的函数
* 参数rejected,用来处理错误用的
* 为什么这么设计,因为拦截器要通过Promise处理
* 返回本条拦截器在数组handlers中的索引位置,以便提供给删除拦截器用
*/
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};
/**
* 通过注册时候返回的拦截器索引来删除拦截器
*/
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
/**
* 遍历拦截器的方法
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};注册完成了,那么怎么发挥作用呢,那就要看看request方法了。
六、request方法,建立Promise链
request方法是Axios的核心方法。接下来我们看看方法都实现了什么。打开lib / core / axios.js,我们看一下主要代码:
var dispatchRequest = require('./dispatchRequest');
Axios.prototype.request = function request(config) {
// 刚进入方法,首先要处理config这里存放着http请求的必要配置
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';
}
// 到这里处理完用于http请求的配置数据config
// 先定义个数组,先放入一个dispatchRequest和undefined,
// dispatchRequest前面的项目目录介绍里提到了,用来申请http请求用的
// 为什么先建立个数组呢,作者的目的就是想先把拦截器和http请求先排好序
// 然后再建立promise调用链,就可以一步一步的按顺序进行了
// 入果没有理解,建议先深入研究一下Promise链式调用
// 为什么先放dispatchRequest,又放个undefined呢
// 可以先看一下下面怎么向chain插入拦截器的
// 拦截器被插入到数组,并且一次向数组插入两个方法,interceptor.fulfilled, interceptor.rejected
// 再看看后面建立promise链式调用的时候,分别用在了then的两个参数,是从数组中一起取两个的
// 所以为了保证拦截器两个方法配对正确所以先插入[dispatchRequest, undefined]
// 之所以用undefined,因为这里没法处理错综复杂而且多变的错误。而且这里也只能用来处理请求拦截器的错误。所以没有必要。
// 所以用undefined,把错误抛到下面的promise,由用户定义处理方法。
var chain = [dispatchRequest, undefined];
// 先初始一个promise,value是config,
// 提供给下面的promise用,也就是提供给请求拦截器用
var promise = Promise.resolve(config);
// 向chain数组插入请求拦截器。一对一对的插入
// 注意这里是从前插入请求拦截器的
// 所以用的时候,先注册的请求拦截器是后执行的,这点要注意
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 向chain数组插入相应拦截器。一对一对的插入
// 相应拦截器是从后插入的
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// 到此,把拦截器和http请求拍好顺序了
// 下面就利用这个循序建立一个promise链
// promise链让拦截器和http请求按照顺序执行了,执行顺序是:
// 请求拦截器->http请求->相应拦截器
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};七、数据修改器
import axios from 'axios'
// 往现有的请求转换器里增加转换方法
axios.defaults.transformRequest.push((data, headers) => {
// ...处理data
return data;
});
// 重写请求转换器
axios.defaults.transformRequest = [(data, headers) => {
// ...处理data
return data;
}];
// 往现有的响应转换器里增加转换方法
axios.defaults.transformResponse.push((data, headers) => {
// ...处理data
return data;
});
// 重写响应转换器
axios.defaults.transformResponse = [(data, headers) => {
// ...处理data
return data;
}];
// 修改某次axios请求的转换器
// // 往已经存在的转换器里增加转换方法
axios.get(url, {
// ...
transformRequest: [
...axios.defaults.transformRequest, // 去掉这行代码就等于重写请求转换器了
(data, headers) => {
// ...处理data
return data;
}
],
transformResponse: [
...axios.defaults.transformResponse, // 去掉这行代码就等于重写响应转换器了
(data, headers) => {
// ...处理data
return data;
}
],
})上面的数据修改器注册的方法就是在向config中的(defaults就是默认的config)transformRequest或transformResponse插入方法或者直接重新定义修改器数组
我们来看看默认配置lib / defaults.js里关于数据修改去都怎么定义的吧:
var defaults = {
transformRequest: [function transformRequest(data, headers) {
// 省略了代码
// 修改之后要返回修改之后的data,因为需要重新给data赋值,
// 不用返回headers,因为headers是对象,修改对象本身,对象就改变,
// data有可能不是对象,修改之后要重新复制给config.data
return data;
}],
transformResponse: [function transformResponse(data) {
// 省略了代码
// 返回data,因为response.data不一定是对象。所以修改后要重新复制
return data;
}],
};在默认配置里,已经定义了数据修改器,所以我们要想添加新的修改器就push新的就行,也可以重新替换掉这个数组,自己重新定义
八、dispatchRequest解析-数据修改器执行原理
module.exports = function dispatchRequest(config) {
// 执行请求数据修改器
// 这里重新给config.data赋值
// 所以定义数据修改器的时候要返回data
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
// 这里是获取http请求方法的
var adapter = config.adapter || defaults.adapter;
// adapter(config)是进行http请求
// 方法返回promise对象,将这个promise作为dispatchRequest返回值,
// 用于后面的相应拦截器处理
return adapter(config).then(function onAdapterResolution(response) {
// 请求成功,执行响应数据修改器
// 这里要给response.data重新赋值,所以定义响应数据修改器要返回data
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
// 请求有误,也执行响应数据修改器
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
};上面源码看到在http请求之前,执行了transformRequest请求数据修改器,在请求之后无论http请求结果成功还是失败,都执行了transformResponse请求数据修改器,都是放在transformData方法中执行的
transformData方法内部怎么实现的我们看一下。
module.exports = function transformData(data, headers, fns) {
/*eslint no-param-reassign:0*/
utils.forEach(fns, function transform(fn) {
data = fn(data, headers);
});
return data;
};看一下很简单,就是遍历数据修改器,把data和headers为参数传给定义的数据修改器处理,处理的结果返回给data,交由下一个修改器处理,直到修改器都执行完,最后返回处理好的data。
九、拦截器和数据修改器的区别
看到这里你是不是有个疑问。之前我们定义的请求拦截器就能修改整个congfig,响应拦截器能修改整个response,请求数据修改器只能修改config.data和config.headers,响应数据修改器只能修改response.data。何必又修改器的功能呢?
根据源码可以得出以下几点,这里是个人的一点儿分析:
第一,看拦截器和数据修改器的参数,明显拦截器的范围更大一些。所以拦截器侧重整体配置的修改。而数据修改器,侧重于对操作的数据进行修改。数据修改器和应用程序的耦合度更高。
第二,拦截器是在Axios构造函数中定义的,是axios对象的属性,在config中不能配置。而数据修改器是在config中定义的。
这能说明什么?
先看数据修改器,可以在axios请求的时候配置在congfig中,在执行Request方法开始的时候就把默认的修改器覆盖成自定义的修改器,不会修改默认配置,下次axios请求的时候如果没有配置修改器,还是用默认的修改器。这样就可以在每一次axios请求中都配置不同的修改器,每次axios请求互补干扰,而且每次axios请求中配置config即可,一条语句解决问题,不需要多于的语句。
而拦截器就不同了,没有这么灵活,一次注册只要不主动删除每次axios请求都会用到注册的拦截器,这就强调了拦截器全局定义的特性。如果想每次axios请求的拦截器都有变化,就会很不方便,而且难免处理不当,造成对其他axios请求的干扰。
当然,数据修改器也可以全局设定,就是修改defaults的数据修改器。
总结:
一、拦截器用户配置级别的修改,侧重整体配置层面。数据修改器用于http请求和响应时数据的修改,侧重于应用的数据层面。
二、拦截器侧重全局配置,一次配置全局使用。数据修改器更加灵活,可以全局配置,一次配置全局使用,也可以每次axios请求配置不同的修改器。
十、http请求适配器,自定义http请求适配器
在dispatchRequest中怎么执行修改器搞清楚了,上面的dispatchRequest源码里还有选择http请求适配器并执行请求的过程。这块也给axios的使用者提供了可扩展的方式。有必要提一下
源码中定义适配器是这样的:
// 这里是获取http请求方法的
// 先判断config中知否有自定义适配器,如果没有则调用默认的适配器
var adapter = config.adapter || defaults.adapter;先判断config中知否有自定义适配器,如果没有则调用默认的适配器。
所以我们可以通过定义config的adapter属性定义自己的http请求。可以每次申请的时候定义,也可以通过拦截器定义全局的http请求适配器。这里不做扩展,大家可以思考一下。
默认适配器怎么调用,看看defaults.adapter源码什么样,打开lib/defaults.js
// getDefaultAdapter方法通过判断是浏览器还是node环境选择相应的适配器
// node环境用./adapters/http
// 浏览器环境用./adapters/xhr
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;
}
var defaults = {
// 默认用getDefaultAdapter方法选择适配器
adapter: getDefaultAdapter(),
}源码中默认给出了两套http请求适配器,node环境下用http适配器,浏览器环境用xhr适配器。具体源码是lib/adapters/文件夹中的http.js和xhr.js。这里不做具体分析,很简单。都是很独立的功能。
运行完dispatchRequest之后就是运行响应拦截器,最后返回Promise链,得到一个Promise。这里就有我们需要的结果,之后处理。上面的六、request方法,建立Promise链详中详细分析了,可以回看
到此整个核心框架、流程就都有了:入口》响应拦截器》响应数据修改器》http请求》响应数据修改器》响应拦截器》返回Promise
总结了一个流程图帮助大家理解记忆:

十一、终止Axios请求
// 第一种取消方法
axios.get(url, {
cancelToken: new axios.CancelToken(cancel => {
if (/* 取消条件 */) {
cancel('取消日志');
}
})
});
// 第二种取消方法
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get(url, {
cancelToken: source.token
});
source.cancel('取消日志');咦!整这么复杂干啥,直接在axios实例里设置个开关,true为继续,false为终止,流程里每一步判断一下不就行了么。这样想就有点考虑简单了。
先看看源码,都在哪里埋下终止的控制器了,详细解释请看代码里的注释
lib/core/dispatchRequest.js
// 这个是判断是否停止的同步方法
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
module.exports = function dispatchRequest(config) {
// 进入dispatchRequest方法要判断一下,
// 这个时候刚执行完请求拦截器
throwIfCancellationRequested(config);
// 然后执行请求数据修改器
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
var adapter = config.adapter || defaults.adapter;
// 接下来http请求。
return adapter(config).then(function onAdapterResolution(response) {
// http请求成功,服务器返回之后判断一下,这时候还没执行响应数据修改器
throwIfCancellationRequested(config);
// 执行响应数据修改器
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
// http请求出问题,并且判断是用户终止axios的问题,
// 判断是否终止
throwIfCancellationRequested(config);
// 执行响应数据修改器,
// 这个时候有可能数据已经返回了所以还是要处理一下数
// 因为有可能用到,这要看用户自己处理了,提供了处理数据,备用
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});所以在执行数据修改器之前,都埋了终止控制器throwIfCancellationRequested
lib/adapters/xhr.js
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var request = new XMLHttpRequest();
// 省略了很多无关代码
// 判断是否设置了cancelToken
if (config.cancelToken) {
// 设置了cancelToken
// cancelToken有promise属性,promise状态变为fulfilled说明需要终止
// 这是异步执行的判断呦,即使现在是终止状态,
// 下面的request.send也会执行,
// 为啥不像dispatchRequest中直接同步判断,
// 如果终止 后面的request.send不执行,不就起到终止的效果了么
// 为啥要这样,即使终止了也要send出去
// 原因是这样的
// 作者这样处理的目的不是要阻止request.send
// 而是要控制从request.send到服务器返回结果,这个阶段的终止
// 你细品,细品,品
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
// Send the request
request.send(requestData);
});
};lib/adapters/http.js 原理和lib/adapters/xhr.js 一样。
所以总结终止方案的需求:
一、是个容器,有状态,是否终止的状态
二、能同步判断知否终止,也能异步判断知否终止
三、在axios文档里有一句话(注意: 可以使用同一个 cancel token 取消多个请求)所以要满足这个注意事项。
所以就单独出一个cancelToken对象,axios内部识别cancelToken对象里面的状态,从而控制请求终止。
看看源码lib/cancel/CancelToken.js怎么实现的吧
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
// 定义一个promise属性,把resolve拿出来备用
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
// 把控制状态的方法cancel传给回调函数
// 这样外部就可以控制改变CancelToken的状态了
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
/**
* 这是个语法糖,可以直接调用就可以返回CancelToken对象
* 和控制状态的方法cancel
*/
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};总结了一个CancelToken在这个axios流程中的作用的路程图,希望帮助大家理解:

以上就是个人通过对axios源码阅读之后总结出来的,希望能给大家有所帮助,也希望能得到指点和吐槽。