Axios是神马🐎?
axios一个基于 Promise 来管理 http 请求的简洁、易用且高效的代码封装库。通俗一点来讲,它是一个前端替代Ajax的一个东西,可以使用它发起http请求接口功能,它是基于Promise的,相比于Ajax的回调函数能够更好的管理异步操作。 源码地址
Axios 的主要特性
基于 Promise
支持浏览器和 node.js环境
可添加请求、响应拦截器和转换请求和响应数据
请求可以取消、中断
自动转换 JSON 数据
客户端支持防范 XSRF
源码目录结构及主要文件功能描述 基于版本0.21.4
├── /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 # 公用工具函数
从入口出发
我们打开/lib/axios.js ,从入口开始分析。
var utils = require('./utils'); var bind = require('./helpers/bind'); var Axios = require('./core/Axios'); var mergeConfig = require('./core/mergeConfig'); var defaults = require('./defaults'); // 创建axios实例的方法 function createInstance(defaultConfig) { // 根据默认配置构建个上下文对象,包括默认配置和请求、响应拦截器对象 var context = new Axios(defaultConfig); // 创建实例 bind后返回的是一个函数,并且上下文指向context var instance = bind(Axios.prototype.request, context); // 拷贝prototype到实例上 类似于把Axios的原型上的方法(例如: request、get、post...)继承到实例上,this指向为context utils.extend(instance, Axios.prototype, context); // 拷贝上下文对象属性(默认配置和请求、响应拦截器对象)到实例上, this指向为context utils.extend(instance, context); // 创建axios实例,一般axios封装 应该都会用到 (我们把一些默认、公共的配置都放到一个实例上,复用实例,无需每次都重新创建实例) instance.create = function create(instanceConfig) { // 这里mergeConfig 就是用来深度合并的 return createInstance(mergeConfig(defaultConfig, instanceConfig)); }; // 返回实例 return instance; } // 创建实例 defaulst为默认配置 var axios = createInstance(defaults); // 向外暴露Axios类,可用于继承 (本人暂未使用过) axios.Axios = Axios; // 这里抛出 中断/取消请求的相关方法到入口对象 axios.Cancel = require('./cancel/Cancel'); axios.CancelToken = require('./cancel/CancelToken'); axios.isCancel = require('./cancel/isCancel'); // 并发请求 完全就是用promise的能力 axios.all = function all(promises) { return Promise.all(promises); }; // 和axios.all 共同使用,单个形参数组参数转为多参 =====> 后面有详解!!! axios.spread = require('./helpers/spread'); // 用作监测是否为Axios抛出的错误 axios.isAxiosError = require('./helpers/isAxiosError'); // 导出 module.exports = axios; // 允许在ts中使用默认导出 module.exports.default = axios;
createInstance
通过入口文件的分析我们可以发现:
我们平常开发中直接使用axios.create()构建的实例和直接axios(),都是通过createInstance这个函数构造出来的。
这个函数大概做了如下几件事:
首先是根据默认配置构建个上下文对象,包括默认配置和请求、响应拦截器对象
创建实例 bind后返回的是一个函, 所以我们使用时可以axios(config)这么使用,并且上下文指向context
拷贝prototype到实例上 类似于把Axios的原型上的方法(例如: request、get、post...)继承到实例上,使用时才可以axios.get()、axios.post(),this指向为context
拷贝上下文对象属性(默认配置和请求、响应拦截器对象)到实例上, this指向为context
返回实例(实例为函数)
axios.create
针对axios.create 方法,正当整理写此篇解析文章期间,发现了 于2021年9月5号有了这么一条PR更新,也就是三天前,为什么这么做那:是为了大型应用、或多域使用多实例情况下, 可以针对已经构造的实例再次封装构造,提供深度构造控制器能力:详情见此条PR
axios
的常使用的api
请求方法通过下面这行代码挂到的axios
上的。
借此我们到/lib/core/Axios.js中看下Axios.prototype
上挂了哪些东西:
// 主请求 方法 所有请求最终都会指向这个方法 Axios.prototype.request = function request(config) { } //内容后面详解 // 获取完成的请求url方法 Axios.prototype.getUri = function getUri(config) { }; // 这里将普通请求(无body数据)挂到prototype上 utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) { Axios.prototype[method] = function(url, config) { // 最终都调用request方法 return this.request(mergeConfig(config || {}, { method: method, url: url, data: (config || {}).data })); }; }); // 这里将有body数据的请求挂到prototype上 utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) { Axios.prototype[method] = function(url, data, config) { // 最终都调用request方法 return this.request(mergeConfig(config || {}, { method: method, url: url, data: data })); }; });ype.request = function request(config) { } //内容后面详解
Axios.prototype
上挂在了9个方法,包括我们常用的下面这些方法。
axios.request(config) axios.get(url[, config]) axios.delete(url[, config]) axios.head(url[, config]) axios.options(url[, config]) axios.post(url[, data[, config]]) axios.put(url[, data[, config]]) axios.patch(url[, data[, config]])
这里请求方法分为了两种分别遍历挂到prototype上,是因为最后面的的这三个方法是可能有请求体的,并且入参形式不同,所以要分开处理。
Axios.prototype.request
接下来我们深入到核心请求方法Axios.prototype.request上,这个方法是可以说是整个axios请求的核心骨架,这里面主要做了对不同config的适配,以及关键的核心链式调用实现。我们进入到代码中查看:
Axios.prototype.request = function request(config) { // 判断参数类型 以支持不同的请求形式axios('url',config) / axios(config) if (typeof config === 'string') { config = arguments[1] || {}; config.url = arguments[0]; } else { config = config || {}; } // 配置合并默认配置 config = mergeConfig(this.defaults, config); // 转化请求的方法 转化为小写 if (config.method) { config.method = config.method.toLowerCase(); } else if (this.defaults.method) { config.method = this.defaults.method.toLowerCase(); } else { config.method = 'get'; } var transitional = config.transitional; if (transitional !== undefined) { // 针对性配置检测 1.0.0版本以后 transitional配置将移除 (好奇目前距离1.0版本好像距离很远,不知为何) validator.assertOptions(transitional, { silentJSONParsing: validators.transitional(validators.boolean, '1.0.0'), forcedJSONParsing: validators.transitional(validators.boolean, '1.0.0'), clarifyTimeoutError: validators.transitional(validators.boolean, '1.0.0') }, false); } // ........ 下面的内容有比较大的更新,单独拆出来详解!!!! };
promise链构成
我们来先看一下原来的构成promise链的经典代码:
// 创建存储链式调用的数组 首位是核心调用方法dispatchRequest,第二位是空 var chain = [dispatchRequest, undefined]; // 创建 promise 为什么resolve(config)是因为 请求拦截器最先执行 所以 设置请求拦截器时可以拿到每次请求的所有config配置 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); }); // 循环 每次取两个出来组成promise链.then执行 while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } // 返回promise return promise;
用图来描述上面这块代码,这样就比较清晰了,整个promise链可以理解为从左到右执行:
请求拦截器 ===> 请求 ===> 响应拦截器
一个新的PR
链式调用骨架这里在6个月前一个新的pr,重构了这部分的代码逻辑,这个pr内容很大,你忍一下:
这里主要是针对了请求拦截器可能会出现异步情况、或有很长的宏任务执行,并且重构之前的代码中,因为请求事放到微任务中执行的,微任务创建的时机在构建promise链之前,如果当执行到请求之前宏任务耗时比较久,或者某个请求拦截器有做异步,会导致真正的ajax请求发送时机会有一定的延迟,所以解决这个问题是很有必要的。
延迟,所以解决这个问题是很有必要的。
// 请求拦截器储存数组 var requestInterceptorChain = []; // 默认所有请求拦截器都为同步 var synchronousRequestInterceptors = true; // 遍历注册好的请求拦截器数组 this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { // 这里interceptor是注册的每一个拦截器对象 axios请求拦截器向外暴露了runWhen配置来针对一些需要运行时检测来执行的拦截器 // 如果配置了该函数,并且返回结果为true,则记录到拦截器链中,反之则直接结束该层循环 if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) { return; } // interceptor.synchronous 是对外提供的配置,可标识该拦截器是异步还是同步 默认为false(异步) // 这里是来同步整个执行链的执行方式的,如果有一个请求拦截器为异步 那么下面的promise执行链则会有不同的执行方式 synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous; // 塞到请求拦截器数组中 requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); }); // 响应拦截器存储数组 var responseInterceptorChain = []; // 遍历按序push到拦截器存储数组中 this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); }); var promise; // 如果为异步 其实也是默认情况 if (!synchronousRequestInterceptors) { // 这里和重构之前的逻辑是一致的了 var chain = [dispatchRequest, undefined]; // 请求拦截器塞到前面 Array.prototype.unshift.apply(chain, requestInterceptorChain); // 响应拦截器塞到后面 chain = chain.concat(responseInterceptorChain); promise = Promise.resolve(config); // 循环 执行 while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } // 返回promise return promise; } // 这里则是同步的逻辑 var newConfig = config; // 请求拦截器一个一个的走 返回 请求前最新的config while (requestInterceptorChain.length) { var onFulfilled = requestInterceptorChain.shift(); var onRejected = requestInterceptorChain.shift(); // 做异常捕获 有错直接抛出 try { newConfig = onFulfilled(newConfig); } catch (error) { onRejected(error); break; } } // 到这里 微任务不会过早的创建 也就解决了 微任务过早创建、当前宏任务过长或某个请求拦截器中有异步任务而阻塞真正的请求延时发起问题 try { promise = dispatchRequest(newConfig); } catch (error) { return Promise.reject(error); } // 响应拦截器执行 while (responseInterceptorChain.length) { promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift()); } return promise;
/core/InterceptorManager.js
// 拦截器增加两个配置参数 synchronous、 runWhen InterceptorManager.prototype.use = function use(fulfilled, rejected, options) { this.handlers.push({ fulfilled: fulfilled, rejected: rejected, // 默认情况下它们被假定为异步的 如果您的请求拦截器是同步的,可以通过这个参数默认配置,它将告诉 axios 同步运行代码并避免请求执行中的任何延迟。 synchronous: options ? options.synchronous : false, // 如果要基于运行时检查执行特定拦截器,可以通过这个runWhen这个参数,类型为函数 runWhen: options ? options.runWhen : null }); return this.handlers.length - 1; };
上面的内容需要反复的梳理,笔者也是结合源码及就该次重构的PR的讨论进行了仔细分析: 详情见此条PR !!!
具体变更对比图
拦截器实现
我们在实际使用axios中,请求、响应拦截器是经常使用的,这也是axios的特点之一。上文中我们分析了promise链的构成,拦截器是何时创建的那,我们在axios.create
中createInstance
去new Axios
实例时构构建出来的。直接上代码:
Axios
实例时构构建出来的。直接上代码:
function Axios(instanceConfig) { this.defaults = instanceConfig; 这里创建的请求和响应拦截器 通过统一的类构造出来的 this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() }; }
我们来进入/core/InterceptorManager.js中:
function InterceptorManager() { this.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; }; // 注销指定拦截器 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) { // 确定没被eject注销 才执行 if (h !== null) { fn(h); } }); }; module.exports = InterceptorManager;
拦截器的实现是比较简单的,通过统一模型,构造统一控制器管理拦截器的注册、注销、执行。
dispatchRequest
我们进入到核心请求方法dispatchRequest中,这里其实结构看起来也就比较简单了:
处理请求头config配置
调用adapter适配器发起真正的请求,针对浏览器环境发起ajax请求,node环境发起http请求
构造响应数据, 会自动转换 JSON 数据
- 构造响应数据, 会自动转换 JSON 数据
function dispatchRequest(config) { // 提前取消请求 throwIfCancellationRequested(config); // 赋个默认值 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 ); // 删除多余的被合并过的数据 utils.forEach( ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'], function cleanHeaderConfig(method) { delete config.headers[method]; } ); // 适配器 axios是可以支持node端也支持浏览器端的 var adapter = config.adapter || defaults.adapter; // 执行请求 return adapter(config).then(function onAdapterResolution(response) { // 提前取消请求情况 throwIfCancellationRequested(config); // 做数据转换 response.data = transformData.call( config, response.data, response.headers, config.transformResponse ); return response; }, function onAdapterRejection(reason) { if (!isCancel(reason)) { throwIfCancellationRequested(config); // 做数据转换 if (reason && reason.response) { reason.response.data = transformData.call( config, reason.response.data, reason.response.headers, config.transformResponse ); } } return Promise.reject(reason); }); };
适配器adapter
经典的设计模式:适配器模式应用。
function getDefaultAdapter() { var adapter; // 判断XMLHttpRequest对象是否存在 存在则代表为浏览器环境 if (typeof XMLHttpRequest !== 'undefined') { // For browsers use XHR adapter adapter = require('./adapters/xhr'); // node环境 使用原生http发起请求 } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { adapter = require('./adapters/http'); } return adapter; }
./adapters/xhr.js 则是对原生ajax XMLHttpRequest对象的的封装,./adapters/http.js 则是对node http模块的封装,也会针对https做相应处理。具体封装细节各种边界细节情况都做了特殊处理 ,因为我们日常还是在浏览器端使用比较多,简单对xhr的封装源码做些整体。
axios主动取消请求
如何使用
这里我们先来看下一个 取消请求如何使用:
import { CancelToken } from axios; // source为一个对象 结构为 { token, cancel } // token用来表示某个请求,是个promise // cancel是一个函数,当被调用时,则取消token注入的那个请求 const source = CancelToken.source(); axios .get('/user', { // 将token注入此次请求 cancelToken: source.token, }) .catch(function (thrown) { // 判断是否是因为主动取消而导致的 if (axios.isCancel(thrown)) { console.log('主动取消', thrown.message); } else { console.error(thrown); } }); // 这里调用cancel方法,则会中断该请求 无论请求是否成功返回 source.cancel('我主动取消请求')
源码分析
在lib/axios.js axios
实例对外抛出了三个取消请求的相关接口,我们来看一下涉及取消请求的是三个文件,在/lib/cancel/中 , 分别的作用:
1.Cancel.js : Cancel函数(伪造类),接受参数message其实就是调用source.cancel()中的参数:取消信息 ,原型对象上的__CANCEL__ 属性,是为了标识改请求返回信息为取消请求返回的信息
2.CancelToken.js :CancelToken提供创建token实例注册取消请求能力及提供取消请求方法
3.isCancel.js :用于判断是为为取消请求返回的结果,也就是是否是Cancel实例
我们来主要分析下CancelToken的源码,从执行角度来分析:
1. source方法
// 暴露出token 和 cancel取消方法 CancelToken.source = function source() { var cancel; // 构造CancelToken 的实例,实例上有两个属性一个promise一个reason // 同时把注册的回调函数的参数也是个函数把这个函数的执行权抛使用者调用(cancel) var token = new CancelToken(function executor(c) { cancel = c; }); return { token: token, cancel: cancel }; };
source方法返回的对象中有两个属性:token 为 new CancelToken的一个实例,cancel是, 是new CancelToken时候函数executor的一个参数,是个函数用来在需要的时候调用主动取消请求。我们来分析下CancelToken的源代码。
2. CancelToken构造函数
function CancelToken(executor) { // 类型判断 if (typeof executor !== 'function') { throw new TypeError('executor must be a function.'); } // 创建一个promise的实例 var resolvePromise; this.promise = new Promise(function promiseExecutor(resolve) { // 把resolve方法提出来 当resolvePromise执行时,this.promise状态会变为fulfilled resolvePromise = resolve; }); // 存下this var token = this; // new CancelToken时会立即调用executor方法 也就是 会执行source方法中的cancel = c; // 这里也就是把cancel函数暴露出去了,把取消的时机留给了使用者 使用者调用cancel时候也就会执行函数内的逻辑 executor(function cancel(message) { // 请求已经被取消了直接return if (token.reason) { return; } // 给token(可就是当前this上)添加参数 调用new Cancel构造出cancel信息实例 token.reason = new Cancel(message); // 这里当主动调用cancel方法时,就会把this.promise实例状态改为fulfilled,resolve出的信息则是reason(new Cancel实例) resolvePromise(token.reason); }); }
这里简单梳理下,在CancelToken中 会创建一个promise实例,和一个reason存储取消信息,当使用者调用source.cancel(message)方法时,会将该promise实例状态改为fulfilled,同时根据参数message创建reason错误信息实例,实例上还有__CANCEL__属性,标识他是取消请求返回的信息。
3. 请求中是如何处理的!!!
在adapter中的操作
当我们调用了cancel方法后,我们在请求中是如何进行中断/取消请求的那 在适配器中这样一段代码
可以找到想要的答案。源码地址
// 判断使用者在改请求中是否配置了取消请求的token if (config.cancelToken) { // 如果配置了则将实例上的promise用.then来处理主动取消调用cancel方法时的逻辑 // 也就是说如果ajax请求发送出去之前,这时我们已经给cancelToken的promise注册了.then // 当我们调用cancel方法时,cancelToken实例的promise会变为fulfilled状态,.then里的逻辑就会执行 config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } // 调用 原生abort取消请求的方法 request.abort(); // axios的promise实例进入rejected状态 这里我们可以看到主动取消的请求是catch可以捕获到 reject(cancel); // request置为null request = null; }); } // 真正的请求在这时才发送出去!!! request.send(requestData);
上面是我们axios在请求中,中断请求的方式,那其他的情况下,请求前、请求完成后也是可以提前去做取消的逻辑的,这样也可以避免多余请求发送和不必要的逻辑执行,我们来看下是怎么做的吧。我们先看下CancelToken原型上的throwIfRequested方法:
// CancelToken原型上有个么一个方法 很简单就是直接抛错 将reason抛出
// reason则是根据调用cancel函数的参数 new Cancel的实例
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
们先看下CancelToken
原型上的throwIfRequested
方法:
// CancelToken原型上有个么一个方法 很简单就是直接抛错 将reason抛出 // reason则是根据调用cancel函数的参数 new Cancel的实例 CancelToken.prototype.throwIfRequested = function throwIfRequested() { if (this.reason) { throw this.reason; } };
在我们的核心请求方法dispatchRequest
中:
直接抛错,代表会将axios构建的promise实例状态直接置为rejected,所以直接就走.catch的逻辑了
// 判断如果配置了取消请求的token则就抛出 function throwIfCancellationRequested(config) { if (config.cancelToken) { // 调用抛出错误的方法 config.cancelToken.throwIfRequested(); } } module.exports = function dispatchRequest(config) { // 请求前 throwIfCancellationRequested(config); // ... 省略代码 // 请求中的在上面adapter中 return adapter(config).then(function onAdapterResolution(response) { // 请求完成后 throwIfCancellationRequested(config); // ... 省略代码 }, function onAdapterRejection(reason) { // 请求完成后 if (!isCancel(reason)) { throwIfCancellationRequested(config); // ... 省略代码 } return Promise.reject(reason); }); };
我们就在axios
请求在catch
中通过isCancel
方法判断这个异常是不是取消请求抛出来的,也就是判断他是不是Cancel实例, 从而做相应处理。
我们来通过请求的简要过程来更好的梳理请求是如何取消的:
相信过一遍上面的源码分析和流程图的分析,应该可以对取消请求的原理有粗略的理解,把整个执行流程、细节理清还需反复阅读。
其他小点
并发能力
在官方 axios
中,还提供了axios.all
和axios.spread
这两个方法,主要是为了执行多个并发请求的,用法如下:
function getUserAccount() { return axios.get('/user/12345'); } function getUserPermissions() { return axios.get('/user/12345/permissions'); } axios.all([getUserAccount(), getUserPermissions()]) .then(axios.spread((acct, perms) => { // 两个请求都完成后才会执行回调里的逻辑 }));
我们直接看源码:
1.axios.all方法与Promise.all方法完全是一模一样的,直接就是调用的Promise.all
2.axios.spread
方法接收一个函数作为参数,这个参数函数的参数也是所有请求的响应
// 并发请求 完全就是用promise的能力 axios.all = function all(promises) { return Promise.all(promises); }; // 接受一个函数callback axios.spread = function spread(callback) { // 返回一个新函数 arr其实就是成功返回的数组 return function wrap(arr) { // 把并发请求的返回结果给callback 方便把并发请求返回的数据放在一起做处理像上面例子那样 return callback.apply(null, arr); }; };
工具函数
merge
函数:递归的去合并, 用在合并一些请求config配置信息,实现方法其实和递归实现深拷贝deepClone
类似
function merge(/* obj1, obj2, obj3, ... */) { var result = {}; // 闭包处理逻辑函数 function assignValue(val, key) { // result里有该键值并且 同为普通Object对象类型递归merge if (isPlainObject(result[key]) && isPlainObject(val)) { result[key] = merge(result[key], val); // result里没有 赋值 } 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; }
extend
函数:axios
内部通过它来将一些内置属性和内置方法挂到axiso实例上
function extend(a, b, thisArg) { // 循环 b的属性挂到a上 forEach(b, function assignValue(val, key) { // 如果有具体this指向 并且类型为函数 if (thisArg && typeof val === 'function') { // bind函数调用完返回了一个函数,这个函数内部使用的apply a[key] = bind(val, thisArg); } else { // 直接赋值 a[key] = val; } }); return a; }
总结
本篇文章针对axios
的主要源码都在上文中👆一一详解,如果仔细阅读完的话相信会有一些不错的收获,直接阅读源码是一个生硬的方式,希望通过结合源码与本篇文章,可以帮助读者更好、更快理解axios
源码,以及在之后的开发中更好的使用axios
。