【源码】axios 源码解读

axios 源码解读

axios版本

  • 1.6.7

一、找到入口文件

先看package.json

再看index.js文件

再来看下lib目录

  • /lib/ // 项目源码目
    • /adapters/ // 定义发送请求的适配器
      • http.js // node环境http对象
      • xhr.js // 浏览器环境XML对象
    • /cancel/ // 定义取消请求功能
    • /helpers/ // 一些辅助方法
    • /core/ // 一些核心功能
      • Axios.js // axios实例构造函数
      • createError.js // 抛出错误
      • dispatchRequest.js // 用来调用http请求适配器方法发送请求
      • InterceptorManager.js // 拦截器管理器
      • mergeConfig.js // 合并参数
      • settle.js // 根据http响应状态,改变Promise的状态
      • transformData.js // 转数据格式
  • axios.js // 入口,创建构造函数
  • defaults.js // 默认配置
  • utils.js // 公用工具函数

二、分析axios.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 创建axios实例
function createInstance(defaultConfig) {

// 根据默认配置构建个上下文对象,包括默认配置和请求、响应拦截器对象
const context = new Axios(defaultConfig);

// 创建实例 将上下文对象绑定到Axios.prototype.request上
const instance = bind(Axios.prototype.request, context);

// 将 axios.prototype 复制到 instance
utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});

// 将 context 复制到 instance
utils.extend(instance, context, null, {allOwnKeys: true});

// 用于创建新实例的工厂函数
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};

return instance;
}

// 创建要导出的默认实例
const axios = createInstance(defaults);

1、对比axios的使用方法

方法一

1
2
3
4
5
6
7
8
9
10
import axios from "axios";

axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});

该方法直接将导出的axios对象用于请求,正式因为源码中它返回的是一个通过createInstance创建好的实例对象,只不过你都是使用的默认配置而已。

方法二

1
2
3
4
5
6
7
import axios from "axios";

const http = axios.create({
baseURL: xxx,
method: "post",
headers: {},
});

我们也可以通过axios对象再来创建一个自定义的实例,因为源码中它的实例对象上又挂载了一个create方法,返回的是一个新的实例对象,但是这里我们可以传入自定义的配置了。

1
2
3
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};

2、createInstance方法做了哪些事情

第一行代码

1
const context = new Axios(defaultConfig);
1
2
3
4
5
6
7
8
9
10
class Axios {
constructor(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
// ...
}

根据默认配置对象创建了一个Axios对象,并且包含默认配置、请求和相应的拦截器对象

第二行代码

1
const instance = bind(Axios.prototype.request, context);

这段代码的作用是创建一个绑定了特定上下文的函数。在这里,Axios.prototype.request 是一个函数,context 是一个特定的上下文对象。bind() 函数将这个函数绑定到指定的上下文中,以确保在调用时函数内部的 this 关键字指向该上下文对象。

假设有一个简单的 Axios 类,定义了一个 request 方法如下:

1
2
3
4
5
class Axios {
request() {
console.log(this.baseUrl);
}
}

还有一个 context 对象,包含了一个 baseUrl 属性:

1
2
3
const context = {
baseUrl: 'https://api.example.com'
};

现在,我们可以使用 bind() 来创建一个绑定了上下文的实例:

1
const instance = bind(Axios.prototype.request, context);

这样,instance 实际上就是一个绑定了特定上下文的 request 方法。当我们调用 instance 时,它将使用预定义的 context 中的 baseUrl 属性:

1
instance(); // 输出: https://api.example.com

第三行代码

1
utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});

这行代码是将 Axios 类的原型属性和方法复制到 Axios 实例上,同时保留原始上下文(context)中的属性和方法。这种操作可以用来确保 Axios 实例具有与 Axios 类相同的功能,同时也保留了原始上下文中的属性和方法,以便实例可以使用。

假设 Axios 类定义了一个原型方法 get():

1
2
3
Axios.prototype.get = function() {
console.log('Executing Axios get method');
};

现在,如果我们使用 utils.extend() 来复制这个方法到 Axios 实例上:

1
utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});

那么 instance 实例就会具有 get() 方法:

1
instance.get(); // 输出:Executing Axios get method

第四行代码

1
utils.extend(instance, context, null, {allOwnKeys: true});

与第三行代码类似,都是将不同的对象属性复制到 instance 实例上,它们的差异在于复制的源对象不同,一个是 Axios 类的原型对象,另一个是一个自定义的上下文对象。

3、axios({}) 方法到底是执行了谁

axios对应的就是instance,在createInstance方法中,既给它绑定了create方法,也把它的指针指向了Axios.prototype.request方法;所以当你直接执行时,实际就是执行的request方法。

1
2
3
4
5
function instance(){

}

instance.create = function(){}

是不是设计挺有意思,方法上挂了一个方法的确很少这么玩 ~

4、axios.get、post、put又是在哪实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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();

Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
});

在Axios文件中,有这样两个遍历函数,当这个文件被引入的时候,就自动给Axios添加了这10个方法

1
2
3
4
5
6
7
8
9
10
- delete
- get
- head
- options
- post
- postForm
- put
- putForm
- patch
- patchFrom

三、分析Axios中方法Axios.prototype.request

1
2
3
4
5
6
7
8
9
10
11
class Axios {
constructor(instanceConfig) {
}

async request(configOrUrl, config) {
try {
return await this._request(configOrUrl, config);
} catch (err) {
}
}
}

接着来看this._request(configOrUrl, config);

1
2
3
4
5
6
7
8
9
_request(configOrUrl, config) {
if (typeof configOrUrl === 'string') {
config = config || {};
config.url = configOrUrl;
} else {
config = configOrUrl || {};
}
// ...
}

首先解析了参数configOrUrl,这也是为啥参数可以有不同写法

1
2
3
4
5
axios({
url: 'xxx'
})

axios(url, {})

1、自定义参数序列化

1
2
3
4
5
6
7
8
9
10
11
12
if (paramsSerializer != null) {
if (utils.isFunction(paramsSerializer)) {
config.paramsSerializer = {
serialize: paramsSerializer
}
} else {
validator.assertOptions(paramsSerializer, {
encode: validators.function,
serialize: validators.function
}, true);
}
}

举个例子,假设我们有一个请求需要发送以下参数:

1
2
3
4
5
const params = {
key1: 'value1',
key2: 'value2',
key3: ['value3', 'value4']
};

默认情况下,Axios 会将这个参数对象序列化为以下形式的 URL 查询字符串:

1
?key1=value1&key2=value2&key3=value3,value4

然而我们可以传递自定义的序列化方法传递给 Axios 请求的 paramsSerializer 参数

1
2
3
4
axios.get('/api/data', {
params: params,
paramsSerializer: customParamsSerializer
});:

2、拦截器初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
const requestInterceptorChain = [];
let synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {

synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;

requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});

const responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});

这里将我们通过 axios.interceptors.request.use的拦截器 装入到了 requestInterceptorChain 开始位置。将我们通过 axios.interceptors.response.use的拦截器 装入到了 responseInterceptorChain 结束位置。

同时我们也注意到,定义了一个synchronousRequestInterceptors变量,默认所有拦截器函数都是同步的,但是也可以在注册使用时传递一个参数synchronous来把拦截器变为异步函数。

拦截器注册的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class InterceptorManager {
constructor() {
this.handlers = [];
}
use(fulfilled, rejected, options) {
this.handlers.push({
fulfilled,
rejected,
synchronous: options ? options.synchronous : false,
runWhen: options ? options.runWhen : null
});
return this.handlers.length - 1;
}
}

3、拦截器执行

非全部同步拦截器执行

如果存在异步请求拦截器,则构建拦截器链,并使用 Promise 链式调用来依次执行拦截器的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if (!synchronousRequestInterceptors) {

// 将真实发送请求的函数装入队列,第二个函数为undefined(为什么,等会就明白了)
const chain = [dispatchRequest.bind(this), undefined];

// 将requestInterceptorChain中的拦截器函数装入chain开始位置
chain.unshift.apply(chain, requestInterceptorChain);

// 将responseInterceptorChain中的拦截器函数装入chain结束位置
chain.push.apply(chain, responseInterceptorChain);

len = chain.length;

// 初始化一个promise对象,并且以config作为返回值
promise = Promise.resolve(config);

// 遍历队列
while (i < len) {

// 每次取出队列中的前两个,也就是对应的resolve,reject函数
promise = promise.then(chain[i++], chain[i++]);
}

return promise;

}

至此,明白了,原来队列中的函数都是按对来存放的,存入和读取每一次都是2个;这样在遍历执行时,确保每一个函数都有自己的catch。

上面的例子中,判断了synchronousRequestInterceptors变量,也就是存在异步拦截器的场景,接下来继续向下看,都是同步拦截器的场景。

都为同步拦截器执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

// 执行请求拦截器中的方法
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;
}
}

// 执行请求的方法
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;

我个人对这段代码其实不太理解,为什么不把同步的部分写为跟异步一模一样的实现,因为最终都是构造了一个promise链,后续有思路再来解。

4、执行request方法

不管是异步拦截器还是同步,我们都看到了dispatchRequest方法,没错,它就是我们最终执行请求的方法,接下来一起去看看它做了啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
export default function dispatchRequest(config) {

// 如果请求已被取消,则抛出异常
throwIfCancellationRequested(config);

// 使用 AxiosHeaders 对象处理请求头部
config.headers = AxiosHeaders.from(config.headers);

// 转换请求数据
config.data = transformData.call(
config,
config.transformRequest
);

// 如果请求方法是 post、put 或 patch,则设置请求头部的 Content-Type
if (['post', 'put', 'patch'].indexOf(config.method) !== -1) {
config.headers.setContentType('application/x-www-form-urlencoded', false);
}

// 获取适配器并发送请求
const adapter = adapters.getAdapter(config.adapter || defaults.adapter);

// 使用适配器发送请求,并处理请求成功和失败的情况
return adapter(config).then(function onAdapterResolution(response) {
// 如果请求已被取消,则抛出异常
throwIfCancellationRequested(config);

// 转换响应数据
response.data = transformData.call(
config,
config.transformResponse,
response
);

// 使用 AxiosHeaders 对象处理响应头部
response.headers = AxiosHeaders.from(response.headers);

// 返回响应对象
return response;
}, function onAdapterRejection(reason) {
// 如果请求未被取消,处理请求失败的情况
if (!isCancel(reason)) {
// 如果请求已被取消,则抛出异常
throwIfCancellationRequested(config);

// 如果存在响应对象,则转换响应数据并处理响应头部
if (reason && reason.response) {
reason.response.data = transformData.call(
config,
config.transformResponse,
reason.response
);
reason.response.headers = AxiosHeaders.from(reason.response.headers);
}
}

// 返回 Promise 对象并拒绝原因
return Promise.reject(reason);
});
}

这段代码实现了发送请求的功能,并对请求成功和失败的情况进行了处理。具体步骤包括:

  • 检查是否有请求被取消,如果有则抛出异常。
  • 使用 AxiosHeaders.from() 方法处理请求头部。
  • 转换请求数据。
  • 如果请求方法为 post、put 或 patch,则设置请求头部的 Content-Type。
  • 获取适配器并发送请求。
  • 处理请求成功的情况:
    • 转换响应数据。
    • 使用 AxiosHeaders.from() 方法处理响应头部。
    • 返回响应对象。
  • 处理请求失败的情况:
    • 如果请求未被取消,则处理响应对象中的响应数据和头部。
    • 返回 Promise 对象并拒绝原因。

由于axios可以在浏览器也可以在node中执行,故而它的内部针对两种环境构造了不同的方法用于http连接;

  • ./adapters/xhr.js 是对原生ajax XMLHttpRequest对象的的封装
  • ./adapters/http.js 则是对node http模块的封装

node环境的先不管,直接看下xhr.js的实现

四、xhr.js实现了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
export default isXHRAdapterSupported && function (config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {

// ...

let request = new XMLHttpRequest();

if (config.auth) {
const username = config.auth.username || '';
const password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
requestHeaders.set('Authorization', 'Basic ' + btoa(username + ':' + password));
}

const fullPath = buildFullPath(config.baseURL, config.url);

request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

request.timeout = config.timeout;

function onloadend() {}

if ('onloadend' in request) {

request.onloadend = onloadend;

} else {

request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
setTimeout(onloadend);
};
}

request.onabort = function handleAbort() {};

request.onerror = function handleError() {};

request.ontimeout = function handleTimeout() {};

// ...

request.send(requestData || null);
});
}

这段代码实现了通过 XMLHttpRequest(XHR)发送请求的功能。主要步骤如下:

  • 准备请求数据(requestData)和请求头部(requestHeaders),包括对数据的处理和头部的标准化。
  • 创建 XMLHttpRequest 对象,并配置请求方法、URL、超时时间等信息。
  • 处理 HTTP basic authentication,如果配置中包含 auth 字段,则在请求头部添加 Authorization 字段,以进行基本认证。
  • 设置 XMLHttpRequest 的各种事件处理程序,包括请求成功、请求失败、请求超时、请求取消等。
  • 处理请求中的一些特殊情况,如 FormData 类型的请求数据、低级网络错误、请求超时等。
  • 处理跨站请求伪造(XSRF)保护,如果需要,添加 XSRF 头部。
  • 添加请求头部,并根据配置设置是否携带凭据。
  • 添加响应类型和进度事件处理程序。
  • 处理请求取消,包括取消令牌和 AbortController 信号。
  • 发送请求,并根据情况处理成功或失败的响应。

没错,你肯定看到了这几行代码

1
2
3
4
let request = new XMLHttpRequest();
request.onloadend = onloadend;
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
request.send(requestData || null);

onloadend

1
2
3
4
5
6
7
settle(function _resolve(value) {
resolve(value);
done();
}, function _reject(err) {
reject(err);
done();
}, response);

在onloadend方法中,调用了settle函数,传递了resolve、reject和response;也是文章最开始处讲的根据根据http响应状态,改变Promise的状态,至此一个完整的request结束。


喜欢这篇文章?打赏一下支持一下作者吧!
【源码】axios 源码解读
https://www.cccccl.com/20230112/源码/http/axios 源码解读/
作者
Jeffrey
发布于
2023年1月12日
许可协议