axios.get()到底做了什么?

前言
相信大家在开发过程中,都使用过 axios
来发送网络请求,那在发送一个网络请求的时候,axios
为我们做了哪些事情呢?本文主要以发送 get
请求为例子,来深入解析一下 axios
的源码。
当然了,本文不会解析 axios
所有功能的实现,只会涉及到一些日常开发中比较常用的几个功能:
- 发送
get
请求的两种方式,axios({method: 'get', url: '/api'})
(axios('/api', {method: 'get'})
) 和axios.get('/api')
的实现。 - 请求拦截器和相应拦截器的实现。
- 取消请求的实现
源码准备
我们先从 axios
的 GitHub 仓库 中克隆源码到本地,接着在终端运行命令 npm install
安装依赖,打开 package.json
,找到 main
字段:

我们可以得知 axios
的入口文件是根目录下的 index.js
,打开 index.js
文件,发现它引入了 lib
目录下的 axios.js
文件:

所以,源码开始的地方就是 lib
目录下的 axios.js
文件。接下来就开始在源码中的海洋里开始畅游啦~
源码解析
在 lib/axios.js
文件中,第一行代码和第二行代码中分别引入了 utils.js
文件和 ./helpers/bind.js
文件,utils.js
文件里面包含了 51 个工具类的函数,./helpers/bind.js
文件是对 Function.prototype.bind()
方法的实现, 下面来逐一解析本文将会用到的 utils
工具函数和 Function.prototype.bind()
的实现。
Function.prototype.bind() 方法的实现
函数的 bind()
方法返回的是一个新函数,该函数的 this
指向是 bind()
方法传入的第一个参数,./helpers/bind.js
文件代码实现其实就五行,主要是通过函数的 apply()
方法实现。
export default function bind(fn, thisArg) {
return function wrap() {
return fn.apply(thisArg, arguments);
};
}
这里实现的 bind
函数和函数的 bind()
方法的调用方式有点不同,bind
函数的调用方式就跟我们平时调用函数一样,只不过这里的第一个参数是要改变 this
指向的函数,第二个参数才是 this
指向,最后也是返回一个函数。
utils 工具函数
findKey
findKey
函数用来查找对象上是否存在某个属性名的小写,如果存在,那么直接返回它,否则返回 null
。
const obj = {NaMe: 'jack'};
const originKey = findKey(obj, 'name');
console.log(originKey); // 'NaMe'
findKey
函数有两个参数:
- 要查找属性名的对象
- 属性名的小写
返回值是对象上的属性名或者 null
。
代码实现如下:
function findKey(obj, key) {
key = key.toLowerCase(); // 转换为小写
const keys = Object.keys(obj); // 获取对象的属性名
let i = keys.length;
let _key;
// 循环遍历查找是否存在某个属性名的小写和 key 一样
while (i-- > 0) {
_key = keys[i];
if (key === _key.toLowerCase()) {
return _key;
}
}
return null;
}
isContextDefined
isContextDefined
函数用于判断一个对象是否定义了并且不是全局对象,比如在浏览器环境下:
isContextDefined(window); // false,因为 window 是全局对象
let obj = {};
isContextDefined(obj); // true
isContextDefined
函数的参数只有一个,那就是要判断的对象。
代码实现:
const _global = (() => {
if (typeof globalThis !== "undefined") return globalThis;
return typeof self !== "undefined" ? self : (typeof window !== 'undefined' ? window : global)
})();
const isContextDefined = (context) => !isUndefined(context) && context !== _global;
isArray
isArray
函数用于判断变量是否为数组
isArray([]); // true
isArray({}); // false
代码实现:
const {isArray} = Array;
kindOf
kindOf
函数用能够精确地获取所有变量的数据类型
kindOf('a') // string
kindOf(1) // number
kindOf(true) // boolean
kindOf([]) // array
代码实现:
const { toString } = Object.prototype;
const kindOf = (cache => thing => {
const str = toString.call(thing);
// cache对象用来缓存计算过的值
return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase());
})(Object.create(null));
本质上,kindOf
函数是通过 Object.prototype.toString.call()
获取的变量类型。
kindOfTest
对 kindOf
函数的二次封装,用来创建判断某个变量是否为指定类型的函数。
const isNumber = kindOfTest('Number');
isNumber(1) // true
isNumber("1") // false
代码实现:
const kindOfTest = (type) => {
type = type.toLowerCase();
return (thing) => kindOf(thing) === type
}
isPlainObject
isPlainObject
函数用于判断变量是否为普通的对象。
isPlainObject({}); // true
isPlainObject(new Object()); // true
isPlainObject(new Array()); // false
代码实现:
const { getPrototypeOf } = Object;
const isPlainObject = (val) => {
if (kindOf(val) !== 'object') {
return false;
}
// 获取变量的原型对象
const prototype = getPrototypeOf(val);
return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in val) && !(Symbol.iterator in val);
}
主要的实现思路是通过对象的原型、Symbol.toStringTag
和 Symbol.iterator
不存在普通对象上的条件来做判断的。这里稍微注意一下,如果对象的原型是 null
的话,则是通过 Object.create(null)
方法创建的对象,也属于普通对象。
typeOfTest
用于判断某个变量的类型。接收一个参数,该参数是 typeof
的返回值,返回值是一个函数,该函数用来判断某个变量的类型是否是 typeofTest
函数传入的参数值。
typeOfTest('string')('a') // true
代码实现:
const typeOfTest = type => thing => typeof thing === type;
isFunction
判断变量是否是函数类型。
const noop = () => {}
isFunction(noop) // true
代码实现:
const isFunction = typeOfTest('function');
主要是对 typeofTest
函数的二次封装。
forEach
forEach
函数可以用来遍历数组和对象,在设计模式中称为迭代器模式。
forEach({a: 1, b: 2, c: 3}, function(val, key, obj) {
// 1, a, {a: 1, b: 2, c: 3}
// 2, b, {a: 1, b: 2, c: 3}
// 3, c, {a: 1, b: 2, c: 3}
console.log(val, key, obj);
})
forEach(['a', 'b', 'c'], function(val, index, arr) {
// 'a', 0, [1, 2, 3]
// 'b', 1, [1, 2, 3]
// 'c', 2, [1, 2, 3]
console.log(val, index, arr);
})
forEach
函数有两个必选参数和一个可选参数:
- 第一个参数代表要遍历的对象或数组
- 第二个参数遍历每一个属性或元素时调用的函数,该函数有三个参数,分别代表属性值或元素值,属性名或索引名,对象或数组本身。
- 第三个可选参数是一个对象,其属性只有一个 —
allOwnKeys
,代表是否获取对象自身的所有属性(包括不可枚举的属性)。
代码实现如下:
/**
* 遍历一个数组或一个对象,遍历每个元素时,都会调用一个函数
* @param {Object|Array} obj 要遍历的对象
* @param {Function} fn 遍历每个属性或元素时调用的函数
*
* @param {Boolean} [allOwnKeys] 是否获取对象自身的所有属性(包括不可枚举的属性)
*/
function forEach(obj, fn, {allOwnKeys = false} = {}) {
if (obj === null || typeof obj === 'undefined') {
return;
}
let i; // 索引
let l; // 长度
if (typeof obj !== 'object') {
// 如果不是对象,就将它变为数组
obj = [obj];
}
// 遍历每个属性或元素,并调用传入的回调函数 — fn
if (isArray(obj)) {
for (i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
// 获取对象的属性名
const keys = allOwnKeys ? Object.getOwnPropertyNames(obj) : Object.keys(obj);
const len = keys.length;
let key;
for (i = 0; i < len; i++) {
key = keys[i];
fn.call(null, obj[key], key, obj);
}
}
}
注意,如果传入的遍历对象是原始数据类型或者是函数类型,那么会将其放到一个数组中,再进行遍历。
extend
通过向对象 a
添加对象 b
的属性来扩展对象 a
。
const a = {name: 'jack'};
const b = {age: 18};
extend(a, b, null);
console.log(a); // {name: 'jack', age: 18}
extend
函数有三个必选参数和一个可选参数:
- 第一个参数是要扩展的对象。
- 第二个参数是要从中添加属性到扩展对象的对象。
- 第三个参数是
this
指向,用改变给第二个参数对象中的方法的this
指向。 - 第四个可选参数和
forEach
的第三个可选参数的意义一样。
代码实现:
/**
* 通过向对象 a 添加对象 b 的属性来扩展对象 a。
*
* @param {Object} a 要扩展的对象
* @param {Object} b 要从中添加属性到扩展对象的对象
* @param {Object} this 指向,用改变给第二个参数对象中的方法的 this 指向
*
* @param {Boolean} [allOwnKeys] 是否获取对象自身的所有属性(包括不可枚举的属性)
* @returns {Object} 对象 a
*/
const extend = (a, b, thisArg, {allOwnKeys}= {}) => {
forEach(b, (val, key) => {
if (thisArg && isFunction(val)) {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
}, {allOwnKeys});
return a;
}
代码的实现思路其实就是遍历 b
对象,将 b
对象中属性值一一赋值给 a
对象,如果属性值是函数的话,就调用上文介绍到的 bind
函数来改变 this
指向。
merge
merge
用来合并多个对象的属性。它的功能很像 Object.assign()
方法,区别在于,对于普通对象来说,Object.assign()
方法是浅合并,而 merge
是深合并。
const obj1 = { a: 0, b: { c: 0 } };
const obj2 = Object.assign({}, obj1);
obj1.b.c = 3;
console.log(obj1.b.c); // 3
const obj3 = { a: 0, b: { c: 0 } };
const obj4 = merge({}, obj1);
obj3.b.c = 3;
console.log(obj4.b.c); // 0
代码实现:
function merge(/* obj1, obj2, obj3, ... */) {
const {caseless} = isContextDefined(this) && this || {}; // 是否忽略大小写
const result = {}; // 合并结果
const assignValue = (val, key) => {
const targetKey = caseless && findKey(result, key) || key;
if (isPlainObject(result[targetKey]) && isPlainObject(val)) {
// 如果属性值都是普通对象的情况下,那么再次调用 merge 函数达到深合并的效果
result[targetKey] = merge(result[targetKey], val);
} else if (isPlainObject(val)) {
// 第一次合并的变量就是对象,也要调用 merge 函数进行深合并
result[targetKey] = merge({}, val);
} else if (isArray(val)) {
// 数组不会进行深合并,仅仅是通过 slice 方法进行浅合并
result[targetKey] = val.slice();
} else {
// 其他对象或原始数据类型直接赋值
result[targetKey] = val;
}
}
// 遍历每个对象来进行属性合并,后一个对象的属性会覆盖前一个对象的属性
for (let i = 0, l = arguments.length; i < l; i++) {
arguments[i] && forEach(arguments[i], assignValue);
}
return result;
}
最核心的代码逻辑就是 assignValue
函数,里面的四个合并条件我也标明了注释。
axios 实例
在熟悉以上 axios
的工具函数之后,我们就来看看 lib/axios.js
的第一段代码 — 创建 axios
实例。
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
// 省略代码...
}
// Create the default instance to be exported
const axios = createInstance(defaults);
通过调用 createInstance(defaults)
函数来创建一个 axios
实例,其中 defaults
表示 axios
的默认配置,比如 transformRequest
、transformResponse
、timeout
、xsrfCookieName
、xsrfHeaderName
、maxContentLength
、maxBodyLength
、validateStatus
、headers
等等属性的默认值。
那么 createInstance
函数内部做了什么事情呢?
首先,通过 new
操作符得出 Axios
类的对象,也就是 const context = new Axios(defaultConfig);
这一行代码的作用,那我们顺着来看看 Axios
类内部做了什么事情。
// lib/core/Axios.js
class Axios {
constructor(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(), // 请求拦截器
response: new InterceptorManager() // 响应拦截器
};
}
// 核心方法
request(configOrUrl, config) {/*...*/}
// 省略代码...
}
// 省略代码...
可见,Axios
类在 constructor
方法做了两件事:
- 保存默认配置
- 初始化请求拦截器和响应拦截器(下文会解析如何实现拦截器)
Axios
类还有个 request
公共方法,它是 axios
库最核心的方法,它会在发送请求之前做一些预处理,以及用来发送请求,也就是说,不管发送任何类型的请求,最后都会调用 request
方法。这个方法会在下文进行详细的解析,目前我们对它有一个大概的了解就行。
在 Axios
类所处的文件中,你还会发现以下这两段代码:
// lib/core/Axios.js
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(mergeConfig(config || {}, {
method,
url,
data: (config || {}).data
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
function generateHTTPMethod(isForm) {
return function httpMethod(url, data, config) {
return this.request(mergeConfig(config || {}, {
method,
headers: isForm ? {
'Content-Type': 'multipart/form-data'
} : {},
url,
data
}));
};
}
Axios.prototype[method] = generateHTTPMethod();
// 省略代码...
});
mergeConfig
函数是用来合并两个配置对象用的,其代码实现位于 lib/core/mergeConfig.js
中,在这里就不重点解析了。它其实是对 utils
中 merge
函数的扩展,在代码设计模式上称为装饰器模式,在不改变 merge
函数的基础上,通过对其进行包装拓展,使得 mergeConfig
函数可以动态具有更多功能。
上述这两段代码其实就干了一件事:在 Axios
类的原型对象上添加 delete
,get
,head
,options
,post
,put
,patch
方法,并且在内部就是执行了 Axios
类的 request
方法。
在 Axios
类的原型对象上添加的这些函数为 axios.get()
这种请求方式打下了基石。
得到 Axios
类的对象之后,我们再来看看 createInstance
函数的下一步做了什么:
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
const instance = bind(Axios.prototype.request, context);
// 省略代码...
return instance;
}
通过 bind
函数将 Axios
类中的 request
方法赋值给 instance
变量函数,也就是说调用 instance
函数相当于调用 Axios
类中的 request
方法,并且 instance
函数的 this
指向是上一次得出的 Axios
类对象。createInstance
函数最终的返回值就是这个instance
函数。
也就是说 axios
其实是一个函数而不是一个对象。之前说过,无论发送什么类型的请求,最终都会调用 request
方法,所以,这就是为什么在官方文档中可以通过 axios({method: 'get', url: '/api'})
(axios( '/api', {method: 'get'})
) 这种方式发送请求的原因。
那 axios.get('/api')
这种方式又是怎么实现的呢?别急,我们往下看。
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
const instance = bind(Axios.prototype.request, context);
utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
// 省略代码...
return instance;
}
通过 utils.extend()
方法将 Axios.prototype
对象上的属性值扩展到 instance
函数上,结合之前对 Axios
类的分析,想想这时候 Axios.prototype
对象上有哪些属性?没错,除了request
方法,还有 delete
,get
,head
,options
,post
,put
,patch
属性方法。所以,instance
函数就有了这些属性方法,从而就可以使用 axios.get('/api')
这种方式发送请求。
最后,再来简单说一下 createInstance
函数剩下的两段代码:
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
const instance = bind(Axios.prototype.request, context);
utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
utils.extend(instance, context, null, {allOwnKeys: true});
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
return instance;
}
createInstance
函数里面还有最后两段代码,它们主要实现以下这两个功能:
- 全局
axios
配置默认值
axios.defaults.baseURL = 'https://api.example.com';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
- 自定义实例
// 创建实例时配置默认值
const instance = axios.create({
baseURL: 'https://api.example.com'
});
全局 axios
配置默认值的功能主要也是通过 utils.extend()
方法将 Axios
类中的实例对象 context
属性值扩展到 instance
函数上,这样就可以通过 axios
来全局配置默认值了。
自定义实例,则是通过给 instance
函数赋值上 create
方法,内部再次调用 createInstance
函数获得另一个 instance
函数。这种方式在设计模式上称为工厂模式,根据不同的输入返回不同的值。
解析完 axios
实例都做了什么事情之后,我们接着来看看拦截器是如何实现的。
InterceptorManager 拦截器
在分析拦截器的源码之前,我们先回顾一下拦截器的使用方法。
添加拦截器
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
}
);
移除拦截器
const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
// eject 方法移除某个拦截器
axios.interceptors.request.eject(myInterceptor);
源码解析
在上文分析 Axios
类时,就提到了在 constructor
方法内初始化了请求和响应拦截器:
class Axios {
constructor(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
// 省略代码
}
那现在来看看 InterceptorManager
的源码吧!
源码位于 lib/core/InterceptorManager.js
文件中
class InterceptorManager {
constructor() {
// 对象数组,每个对象存放了拦截器的 fulfilled 和 rejected 函数
this.handlers = [];
}
// 添加拦截器的 fulfilled 和 rejected 函数
use(fulfilled, rejected, options) {
// 将 fulfilled 和 rejected 函数当作对象的属性存入 handlers 数组中
this.handlers.push({
fulfilled,
rejected,
// synchronous 属性表示拦截器函数是否同步执行
synchronous: options ? options.synchronous : false,
// 省略代码...
});
return this.handlers.length - 1;
}
// 移除某个拦截器
eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
}
// 清除拦截器
clear() {
if (this.handlers) {
this.handlers = [];
}
}
// 遍历所有的拦截器
forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
}
}
通过以上源码,我们得出以下信息:
- 当使用
axios.interceptors.request.use()
或axios.interceptors.response.use()
添加拦截器时,并没有执行拦截器,而是将它先存放到handlers
数组上,那么拦截器什么时候执行呢?依然是在Axios
类的request
方法中执行,下文会详细地解析这个方法。 axios.interceptors.request.use()
或axios.interceptors.response.use()
的返回值也不是什么拦截器的id
,而是这个拦截器在handlers
数组中的索引。- 移除拦截器的逻辑是根据索引将
handlers
中的拦截器元素设置为null
值,因此在使用forEach
方法遍历handlers
数组时,需要判断该拦截器元素不为空才执行后续的代码。那为什么不使用数组的splice
方法来删除呢?因为这个方法的时间复杂的是O(n)
,n
为handlers
数组的长度。而this.handlers[id] = null
的时间复杂度是O(1)
,效率高太多了。 handlers
数组的拦截器元素中的synchronous
属性表示拦截器函数是否同步执行,true
为同步执行,false
为异步执行,默认值为false
。
request 方法
前面说过,Axios
类中的 request
方法是最核心的方法,任何类型的请求方式最终都是调用这个方法,比如 axios.get('/api')
或者 axios({url: '/api', method: 'get'})
(axios('/api', {method: 'get'})
),这时候我们就会有所疑问,那这些方式最终都会调用 request
方法,而传入参数的类型有可能是一个字符串,也有可能是一个普通对象,或者是一个字符串和一个对象,那怎么去做参数的兼容性处理呢?让我们来看看 request
的处理方式。
request(configOrUrl, config) {
if (typeof configOrUrl === 'string') {
// 如果第一个参数是字符串
config = config || {};
config.url = configOrUrl;
} else {
// 如果第一个参数是对象
config = configOrUrl || {};
}
// 省略代码...
}
request
的处理方式非常巧妙,其逻辑是:无论如何,都要保持 config
变量的值为对象。如果第一个参数的类型为字符串,那么就将第一参数值当作 config
对象的 url
属性值,否则就把第一个参数值赋值给 config
变量。
接下来,request
方法主要还对 config
对象做了以下预处理:
- 合并默认配置和用户自定义配置
- 保证
config
对象中method
属性值是小写,如:get
。 - 将
config
对象中headers
属性值的common
属性和[config.method]
属性合并为一个对象。 - 删除
config
对象中headers
属性值的delete
,get
,head
,post
,put
,patch
,common
属性,因为它们已经在存在于config
对象的method
字段中。
request(configOrUrl, config) {
if (typeof configOrUrl === 'string') {
// 如果第一个参数是字符串
config = config || {};
config.url = configOrUrl;
} else {
// 如果第一个参数是对象
config = configOrUrl || {};
}
// 合并默认配置和自定义配置
config = mergeConfig(this.defaults, config);
// 省略代码...
// 保持 method 属性值为小写
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
// 将 headers 中的 common 值和 [config.method] 合并为一个对象
const {headers} = config;
let contextHeaders;
contextHeaders = headers && utils.merge(
headers.common,
headers[config.method]
);
// 删除 'delete', 'get', 'head', 'post', 'put', 'patch', 'common' 属性
contextHeaders && utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
(method) => {
delete headers[method];
}
);
// 合并头信息
config.headers = AxiosHeaders.concat(contextHeaders, headers);
}
处理好 config
对象的属性之后,那么接下来就到了如何执行拦截器的问题了。而在执行拦截器之前,request
方法会将请求拦截器的函数和响应拦截器的函数各保存到一个数组上。
request(configOrUrl, config) {
// 省略 config 预处理的代码
// 是否同步执行拦截器
let synchronousRequestInterceptors = true;
// 保存请求拦截器的 fulfilled 函数和 rejected 函数
const requestInterceptorChain = [];
// 遍历通过 use 方法添加的拦截器函数,并将这些函数加入到 requestInterceptorChain
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 省略代码...
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected)
});
// 保存响应拦截器的 fulfilled 函数和 rejected 函数
const responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
}
这里有两个需要注意的点:
synchronousRequestInterceptors
变量是用来控制拦截器是否同步执行的,默认拦截器是异步执行的(这在上文InterceptorManager
拦截器源码解析中讲到过)。requestInterceptorChain
数组中的请求拦截器处理函数的顺序是和用use
方法添加的处理函数的顺序相反的,也就是说,最后的执行顺序和添加函数的顺序相反,比如:
axios.interceptors.request.use(function (config) {
console.log("1");
return config;
}, function (error) {
console.log("1");
return Promise.reject(error);
});
axios.interceptors.request.use(function (config) {
console.log("2");
return config;
}, function (error) {
console.log("2");
return Promise.reject(error);
});
// 先打印 2,再打印 1。
将拦截器的函数保存到数组之后,执行拦截器的逻辑就比较清晰了,无非就是遍历 requestInterceptorChain
和 responseInterceptorChain
两个数组,执行数组里的每一个元素函数,但执行拦截器的方式有两种,分别是:异步执行和同步执行。
request(configOrUrl, config) {
// 省略 config 预处理的代码
// 省略拦截器预处理的代码
let promise;
let i = 0;
let len;
// 异步执行
if (!synchronousRequestInterceptors) {
// dispatchRequest 函数是正式发送 xhr 请求的函数
const chain = [dispatchRequest.bind(this), undefined];
// 将请求拦截器放在 xhr 请求函数之前
chain.unshift.apply(chain, requestInterceptorChain);
// 将响应拦截器放在 xhr 请求函数之后
chain.push.apply(chain, responseInterceptorChain);
len = chain.length;
promise = Promise.resolve(config);
while (i < len) {
// 通过 promise then 链的执行方式达到异步执行的效果
promise = promise.then(chain[i++], chain[i++]);
}
return promise;
}
// 同步执行
len = requestInterceptorChain.length;
let newConfig = config;
i = 0;
// 同步执行请求拦截器
while (i < len) {
const onFulfilled = requestInterceptorChain[i++];
const onRejected = requestInterceptorChain[i++];
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected.call(this, error);
break;
}
}
// 执行 xhr 请求
try {
promise = dispatchRequest.call(this, newConfig);
} catch (error) {
return Promise.reject(error);
}
// 执行响应拦截器
i = 0;
len = responseInterceptorChain.length;
while (i < len) {
promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]);
}
return promise;
}
这里的 dispatchRequest
函数是用来发送 xhr
请求,返回 Promise
对象,可见它需要在请求拦截器之后和响应拦截器之前执行,dispatchRequest
函数会在下文进行解析。
异步执行拦截器是通过 Promise
的 then
链形式来完成;而同步执行的情况下,是先直接遍历 requestInterceptorChain
数组,执行请求拦截器,然后再执行 dispatchRequest
函数发送 xhr
请求,最后仍然要通过 Promise
的 then
链形式来执行响应拦截器,因为 dispatchRequest
函数返回的是一个 Promise
对象。
好了,以上就是 request
方法中的源码了。最后,简单总结一下 request
方法都做了什么:
- 预处理
config
配置对象 — 对各种请求方式的传参类型做兼容性处理,确保method
属性为小写值,处理头信息属性。 - 将请求拦截器处理函数和响应拦截器处理函数各放在一个数组上。注意,请求拦截器的处理函数的执行顺序跟添加顺序是相反的。
- 根据
synchronousRequestInterceptors
变量判断是同步执行拦截器还是异步执行拦截器,默认是异步执行。
dispatchRequest
根据上文我们知道了 dispatchRequest
函数是用来发送 xhr
请求,其源码位于 lib/core/dispatchRequest.js
文件。其实在 dispatchRequest
函数内,还执行了一个操作 — 取消请求,但这个会留到下文解析。除了取消请求,该函数主要做了以下这几件事:
- 完善头信息字段 —
headers
- 执行
transformRequest
函数数组。transformRequest
允许在向服务器发送前,修改请求数据,只能用于put
,post
和patch
这几个请求方法。 - 设置
Content-Type
的默认值 - 获取请求的方式,是发送浏览器的
xhr
请求还是Node.js
环境中的http
模块请求。 - 发送请求,通过
Promise
对象的then
方法接收响应信息,并执行transformResponse
函数数组,它在通过axios({url: '/api'})
函数或axios.get({url: '/api'})
方法的then
方法获取响应数据之前会执行,允许修改响应数据。
// 如果已经取消请求了,就抛出错误
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
if (config.signal && config.signal.aborted) {
throw new CanceledError(null, config);
}
}
function dispatchRequest(config) {
// 检查是否已经取消请求了
throwIfCancellationRequested(config);
// 完善头信息字段
config.headers = AxiosHeaders.from(config.headers);
// 执行 transformRequest 数组函数
config.data = transformData.call(
config,
config.transformRequest
);
// 设置默认的 Content-Type
if (['post', 'put', 'patch'].indexOf(config.method) !== -1) {
config.headers.setContentType('application/x-www-form-urlencoded', false);
}
// 获取请求方式,当前是 xhr 请求,如果是 node 环境下,则是 http 模块请求
const adapter = adapters.getAdapter(config.adapter || defaults.adapter);
// 发送请求,接收响应信息,执行 transformResponse 函数数组
return adapter(config).then(function onAdapterResolution(response) {
// 检查是否已经取消请求了
throwIfCancellationRequested(config);
// 执行 transformResponse 函数数组
response.data = transformData.call(
config,
config.transformResponse,
response
);
// 完善响应头信息
response.headers = AxiosHeaders.from(response.headers);
return response;
}, function onAdapterRejection(reason) {
// 省略代码...
return Promise.reject(reason);
});
}
本文不会解析
transformRequest
和transformResponse
的源码,感兴趣的朋友可以自行前往lib/defaults/index.js
文件中阅读对应的源码。
着重解析一下是如何根据代码的运行环境来获取请求方式的,也就是下面这一行代码中 getAdapter
方法的实现逻辑。
const adapter = adapters.getAdapter(config.adapter || defaults.adapter);
其中 defaults.adapter
的值是 ['xhr', 'http']
。getAdapter
方法的源码位于 lib/adapters/adapters.js
中,我们来看看它具体做了什么
import httpAdapter from './http.js';
import xhrAdapter from './xhr.js';
// xhr 请求和 http 模块请求
const knownAdapters = {
http: httpAdapter,
xhr: xhrAdapter
}
// 省略代码
getAdapter: (adapters) => {
adapters = utils.isArray(adapters) ? adapters : [adapters];
const {length} = adapters;
let nameOrAdapter;
let adapter;
for (let i = 0; i < length; i++) {
nameOrAdapter = adapters[i];
// 默认情况下,从 knownAdapters 对象中获取请求方式
if((adapter = utils.isString(nameOrAdapter) ? knownAdapters[nameOrAdapter.toLowerCase()] : nameOrAdapter)) {
break;
}
}
// 省略代码
return adapter;
},
从源码上看,axios
默认内置了两种情况方式 — xhr
请求和 http
模块请求,axios
也允许用户自定义请求方式,通过配置对象中的 adapter
属性声明即可。那么假设在当前的浏览器环境下,如何获取这两者的其中一个请求方式呢?
那就需要看看 httpAdapter
和 xhrAdapter
的源码是如何实现的了。它们的源码分别位于 lib/adapters/http.js
和 lib/adapters/xhr.js
,由于本文是以浏览器环境的前提下为主来解析代码且 httpAdapter
的实现逻辑相对复杂,所以本文也不会进行解析,只需要知道它是怎么获取相应的请求方式就行。
在 lib/adapters/http.js
中:
// 省略代码...
const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process';
export default isHttpAdapterSupported && function httpAdapter(config){/*...*/}
可见是通过判断当前环境是否存在 process
变量来决定是否导出 httpAdapter
函数,当前假设的是浏览器环境,所以导出的 false
值。
在 lib/adapters/xhr.js
中:
// 省略代码...
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
export default isXHRAdapterSupported && function (config) {/*省略代码*/}
通过判断当前环境是否存在 XMLHttpRequest
对象来决定是否导出 xhrAdapter
函数,当前假设是浏览器环境,所以导出的是 xhrAdapter
函数。
所以,在 lib/adapters/adapters.js
中,knownAdapters
对象的值实际为:
const knownAdapters = {
http: false,
xhr: xhrAdapter
}
所以getAdapter
方法自然而然地也就获取到了 xhr
请求方式。getAdapter
方法在设计模式中称为适配器模式,将一个对象的接口(方法、属性)转化为用户需要的另一个接口,解决对象之间接口不兼容的问题。
获取到 xhr
请求方式之后,那么接下来就要看看 xhrAdapter
函数是怎么发送 xhr
请求的了。
取消请求与发送请求
发送请求
发送 xhr
请求的源代码位于 lib/adapters/adapters.js
。我们仔细回想一下使用原生的 XMLHttpRequest
对象发送请求的步骤:
- 创建
XMLHttpRequest
对象
const xhr = new XMLHttpRequest();
- 在这个对象上使用
open
方法创建一个HTTP
请求,参数是请求方法、请求地址、是否异步和用户的认证信息。比如创建一个异步的get
请求。
xhr.open('get', '/api', true);
- 在正式发起请求前,为这个对象添加一些信息和监听函数,比如,通过
setRequestHeader
方法来为请求添加头信息;监听onreadystatechange
事件,XMLHttpRequest
对象的状态存在readyState
属性上,当监听到readyState
变为 4 时,代表服务器返回的数据接收完成,这时判断返回的状态,如果状态为 2xx 或者 304 则代表返回正常,可以通过response
中的数据来对页面进行更新。
request.setRequestHeader('Connection', 'keep-alive');
xhr.onreadystatechange = function () {
if(xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText)
}
};
- 最后调用
send
方法来向服务器发起请求,可以传入参数作为发送请求的数据体。
xhr.send();
其实 lib/adapters/adapters.js
的源代码大致上就是围绕这四个步骤去做一些补充,包括设置超时时间,监听 onloadend
事件,监听 onabort
事件,监听 onerror
事件,监听 ontimeout
事件,取消请求等等。
export default isXHRAdapterSupported && function (config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
let requestData = config.data;
// 头信息
const requestHeaders = AxiosHeaders.from(config.headers).normalize();
// 请求期望的响应类型
const responseType = config.responseType;
let onCanceled;
// 服务器返回的数据接收完成后,移除所有取消请求函数
function done() {
if (config.cancelToken) {
config.cancelToken.unsubscribe(onCanceled);
}
if (config.signal) {
config.signal.removeEventListener('abort', onCanceled);
}
}
// 省略代码...
// 创建 XMLHttpRequest 对象
let request = new XMLHttpRequest();
// 省略代码...
// 获取完整的请求路径
const fullPath = buildFullPath(config.baseURL, config.url);
// buildURL 函数将参数添加到请求路径上
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params), true);
// 设置超时时间
request.timeout = config.timeout;
// 请求完成后会执行该函数
function onloadend() {
if (!request) {
return;
}
// 准备响应头
const responseHeaders = AxiosHeaders.from(
'getAllResponseHeaders' in request && request.getAllResponseHeaders()
);
// 服务器返回的数据
const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
request.responseText : request.response;
// 响应数据
const response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
};
if (response.status === 200 || response.status === 304) {
resolve(response);
} else {
// 返回错误信息
reject(new AxiosError('Request failed with status code ' + response.status));
}
done();
// 清除请求对象
request = null;
}
if ('onloadend' in request) {
// 如果 onloadend 事件存在 XMLHttpRequest 对象,就监听 onloadend 事件
request.onloadend = onloadend;
} else {
// 监听 onreadystatechange 事件
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// 省略代码...
/**
* 由于 readystate 事件在 onerror 事件和 ontimeout 事件之前执行,
* 所以最终需要延迟执行 onloadend 函数。
*/
setTimeout(onloadend);
};
}
// 监听 onabort 事件
request.onabort = function handleAbort() {
if (!request) {
return;
}
// 返回错误信息
reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
// 清除请求对象
request = null;
};
// 监听 onerror 事件
request.onerror = function handleError() {
// 返回错误信息
reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));
// 清除请求对象
request = null;
};
// 监听 ontimeout 事件
request.ontimeout = function handleTimeout() {
let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
// 省略代码...
// 返回错误信息
reject(new AxiosError(timeoutErrorMessage));
// 清除请求对象
request = null;
};
// 如果请求体数据为空,移除 Content-Type 请求头信息字段
requestData === undefined && requestHeaders.setContentType(null);
// 设置请求头信息
if ('setRequestHeader' in request) {
utils.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) {
request.setRequestHeader(key, val);
});
}
// 省略代码...
// 取消请求
if (config.cancelToken || config.signal) {
// 取消请求函数
onCanceled = cancel => {
if (!request) {
return;
}
// 如果取消请求对象不存在,返回错误信息
reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
// 取消 xhr 请求
request.abort();
request = null;
};
// 添加 onCanceled 请求函数,onCanceled 函数的调用时机由外部决定
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {
// fetch API — AbortController 取消请求的方式
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
// 省略代码...
// 发送请求
request.send(requestData || null);
});
}
这里关于取消请求的逻辑会在下文马上解析,因为发送请求与取消请求有一定的关联关系,所以需要先在这里展示如何取消请求的逻辑。
取消请求
axios
官方文档中说明了有两种方式可以取消请求,分别是 fetch API 方式 —AbortController
和 CancelToken
。
// AbortController
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// 取消请求
controller.abort()
// CancelToken
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/foo/bar', {
cancelToken: source.token
}).then(function(response) {
//...
});
// 取消请求
source.cancel('Operation canceled by the user.');
AbortController
取消请求的方式在发送请求的源代码中表现得非常简单,只需要对配置对象中的 signal
属性监听 abort
事件即可。
// lib/adapters/xhr.js
// ...
if (config.cancelToken || config.signal) {
// 取消请求函数
onCanceled = cancel => {
if (!request) {
return;
}
// 如果取消请求对象不存在,返回错误信息
reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
// 取消 xhr 请求
request.abort();
request = null;
};
// 省略代码...
if (config.signal) {
// 监听 abort 事件
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
// ...
如果 signal
属性已经是 aborted
状态,那么就直接调用 onCanceled
函数,否则,通过外部调用 controller.abort()
方法,触发 abort
事件调用 onCanceled
函数。
如果最终因为某些原因,没有调用 controller.abort()
方法,那么当请求发送成功,并且响应数据接收完成时,会移除 abort
事件的监听。
// lib/adapters/xhr.js
// ...
function done() {
// 省略代码...
// 移除 `abort` 事件的监听
if (config.signal) {
config.signal.removeEventListener('abort', onCanceled);
}
}
// ...
而 CancelToken
的取消方式源代码稍微复杂一点,它主要运用到了设计模式中的观察者模式。
class CancelToken {
constructor(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
let resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
const token = this;
this.promise.then(cancel => {
if (!token._listeners) return;
let i = token._listeners.length;
// 执行所有订阅者函数
while (i-- > 0) {
token._listeners[i](cancel);
}
// 清除所有订阅者
token._listeners = null;
});
// 省略代码
// cancel 回调函数用于通知所有订阅者
executor(function cancel(message, config, request) {
if (token.reason) {
// 如果取消信息不为空,说明已经取消过请求
return;
}
token.reason = new CanceledError(message, config, request);
// 触发上文的 this.promise.then() 中的回调函数
resolvePromise(token.reason);
});
}
// 如果已经取消请求了,抛出错误
throwIfRequested() {
if (this.reason) {
throw this.reason;
}
}
// 添加订阅者
subscribe(listener) {
if (this.reason) {
/**
* 如果取消信息不为空,则说明已经取消过,
* 应该立马通知订阅者,告诉它已经取消过请求了。
*/
listener(this.reason);
return;
}
if (this._listeners) {
this._listeners.push(listener);
} else {
this._listeners = [listener];
}
}
// 移除订阅者
unsubscribe(listener) {
if (!this._listeners) {
return;
}
const index = this._listeners.indexOf(listener);
if (index !== -1) {
this._listeners.splice(index, 1);
}
}
/**
* 返回一个对象,里面包含了 CancelToken 类的实例对象 token
* 和通知所有订阅者的函数 cancel
*/
static source() {
let cancel;
const token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token,
cancel
};
}
}
在发送请求的源代码中,实现了添加订阅者的步骤:
// lib/adapters/xhr.js
// ...
if (config.cancelToken || config.signal) {
// 取消请求函数
onCanceled = cancel => {
if (!request) {
return;
}
// 如果取消请求对象不存在,返回错误信息
reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
// 取消 xhr 请求
request.abort();
request = null;
};
// 添加订阅者
config.cancelToken && config.cancelToken.subscribe(onCanceled);
// 省略代码...
}
// ...
那什么时候通知所有订阅者呢?还记得一开始举的官方例子吧?其中的 source.cancel('Operation canceled by the user.')
就是用来通知所有订阅者的。之后就会执行 request.abort()
代码,中断 xhr
请求。
同样地,如果最终因为某些原因,没有调用 source.cancel('Operation canceled by the user.')
方法,那么当请求发送成功,并且响应数据接收完成时,会移除订阅者。
// lib/adapters/xhr.js
// ...
function done() {
// 移除订阅者
if (config.cancelToken) {
config.cancelToken.unsubscribe(onCanceled);
}
// 省略代码...
}
// ...
总结
-
无论是用
axios(config)
还是用axios.get(url[, config])
发送请求,其本质上都是在调用Axios
类中的request
方法,request
方法会对各种传参形式做一个统一处理。 -
拦截器默认是异步执行的,如果想要同步执行,则要
axios.interceptors.(request|response).use
方法的第三个参数中设置{ synchronous: true }
。拦截器函数也是在Axios
类中的request
方法的执行。 -
本质上,
axios
就是xhr
请求(浏览器环境下)或者http
模块请求(Node.js
环境下),只不过在正式发送请求之前,做了大量的预处理工作。 -
取消请求的方式分为两种:
AbortController
和CancelToken
,现官方推荐用AbortController
。源代码实现上,AbortController
也比较简单明了,只需要监听abort
事件即可;而CancelToken
则是运用了设计模式中观察者模式的思想,通过调用外部的cancel
方法来通知所有的订阅者,从而执行request.abort()
方法,完成取消请求。 -
axios
中运用了大量的设计模式,比如本文提到过的迭代器模式、装饰器模式、适配器模式、工厂模式、观察者模式。如果想系统学习设计模式的话,推荐曾探老师的《JavaScript设计模式与开发实践》。
转载自:https://juejin.cn/post/7246777363256590393