「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」。
学习原因
axios 作为一个在前端领域非常重要的 ajax 请求库,不管是 Vue、React还是node ,都可以使用axios进行ajax请求,可以说,axios 是前端非常重要的一块,掌握 axios 的原理、对我们日常开发和技术学习有着非常重要的意义。
学习内容
- 了解axios发送请求和实现过程
- 了解axios拦截器的工作原理
- 了解axios取消请求功能
- 模拟上三条功能
学习准备
以下准备是为了模拟功能实现做准备,不模拟可以跳过。
- 使用npm中的json-server包创建一个静态服务器,提供我们开发时需要的接口。
- 可以选中全局安装
npm install -g json-server - 在创建一个
db.json文件 - 将下面内容添加道
db.json中
"posts": [
{
"id": 1,
"title": "json-server",
"author": "typicode"
},
{
"id": 2,
"title": "json-server2",
"author": "typicode2"
}
],
"comments": [
{
"id": 1,
"body": "some comment",
"postId": 1
}
],
"profile": {
"name": "typicode"
}
}
- 使用
json-server --watch db.json启动服务,可以在3000端口访问响应资源http://localhost:3000/posts
开始
- 打开vscode终端下载axios包
npm i axios, 得到的axios主要目录为:
├── /dist/ # 项目输出目录
├── /lib/ # 项目源码目录
│ ├── /cancel/ # 定义取消功能
│ ├── /core/ # 一些核心功能
│ │ ├── Axios.js # axios的核心主类
│ │ ├── dispatchRequest.js # 用来调用http请求适配器方法发送请求
│ │ ├── InterceptorManager.js # 拦截器构造函数
│ │ └── settle.js # 根据http响应状态,改变Promise的状态
│ ├── /helpers/ # 一些辅助方法
│ ├── /adapters/ # 定义请求的适配器 xhr、http
│ │ ├── http.js # 实现http适配器
│ │ └── xhr.js # 实现xhr适配器
│ ├── axios.js # 对外暴露接口
│ ├── defaults.js # 默认配置
│ └── utils.js # 公用工具
├── package.json # 项目信息
├── index.d.ts # 配置TypeScript的声明文件
└── index.js # 入口文件
axios的基本使用
- 在axios中文文档中我们可以看到axios有很多API,
axios(config)、axios.get(config)、axios.post(config)、axios.creat(config)...
axios的运行流程
axios的创建过程
-
axios为什么可以当函数使用,又可以当对象使用?
-
通过index.js入口文件进入,发现主要核心是
/lib/axios,在这个文件中我们可以找到答案
// axios.js
...
function createInstance(defaultConfig) {
/*
创建Axios的实例
原型对象上有一些用来发请求的方法: get()/post()/put()/delete()/request()
自身上有2个重要属性: defaults/interceptors
*/
var context = new Axios(defaultConfig);
// axios和axios.create()对应的就是request函数
// Axios.prototype.request.bind(context)
var instance = bind(Axios.prototype.request, context); // axios
// 将Axios原型对象上的方法拷贝到instance上: request()/get()/post()/put()/delete()
utils.extend(instance, Axios.prototype, context);
// 将Axios实例对象上的属性拷贝到instance上: defaults和interceptors属性
utils.extend(instance, context);
// Factory for creating new instances
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
return instance;
}
// Create the default instance to be exported
var axios = createInstance(defaults);
...
- Axios.js
// Axios构造函数
function Axios(instanceConfig) {
// 将指定的config, 保存为defaults属性
this.defaults = instanceConfig;
// 将包含请求/响应拦截器管理器的对象保存为interceptors属性
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
Axios.prototype.request = function request(config) {
...
};
// 用来得到带query参数的url
Axios.prototype.getUri = function getUri(config) {
...
};
//添加request 方式
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
...
});
- axios为createInstance函数返回的对象
- 在createInstan函数中,定义一个Axios实例context,使用bind将Axios原型上的request方法的this绑定到context上并将它赋值给instance。使得axios可以作为函数使用(axios()...)
- 再将Axios原型对象上的方法拷贝到instance上,使得axios可以作为对象使用(axios.get()...)
- 在将Axios实例上的属性拷贝给instance上。axios可以使用拦截器(axios.interceptors.request.use()...)
- 最后将create方法交给instance,使其可以递归地用于扩展客户端。在连接到多个具有某些共同点但并非全部的远程设备的情况下,可以针对已经构造的实例再次封装构造,提供深度构造控制器能力
模拟axios的创建
// 构造函数
function Axios(config) {
// 初始化
this.defaults = config //为了创建 default 默认属性
this.intercepters = {
request: {},
Response: {}
}
}
// 原型添加相关方法
Axios.prototype.request = function (config) {
console.log("发送ajax请求,请求方法为"+ config.method);
}
Axios.prototype.get = function (config) {
return Axios.prototype.request({method:"GET"})
}
Axios.prototype.post = function (config) {
return Axios.prototype.request({ method: "POST" })
}
// 声明函数
function createInstance(config){
// 实例化一个对象
let context = new Axios(config) // context 可以使用Axios原型上的方法, 如context.get() constext.post,但不能当做函数使用context()
// 创建请求函数
let instance = Axios.prototype.request.bind(context) //bind方法会创建一个新的函数 instance 已经是一个函数, 并且可以使用instance传参发送请求,但只能使用request函数, 而且不能作为对象使用,因为它上面并没有get、post、put等方法和defaults、intercepters属性
// 将 Axios.prototype 对象中的方法添加到instance函数对象中
Object.keys(Axios.prototype).forEach(key =>{
instance[key] = Axios.prototype[key].bind(context) // 使用bind确保Axios.prototype对象中的方法中this指向Axios,能够使用this.defaults、this.intercepters
})
// 为instance 函数对象添加属性 default、 intercepters
Object.keys(context).forEach(key => {
instance[key] = context [key]
})
// 返回 instance 此时的instance既可以当函数使用,也可以调用方法
return instance
}
//测试
let axios = createInstance()
// 发送请求
axios({method:"GEt"})
axios({method:"POST"})
axios.get({})
axios.post({})
- 这里并没有实现request,也没有实现xhr请求,下面会一一模拟
axios请求过程
-
通过上面axios创建过程我们知道axios()实际是Axios.prototype.request方法,那么request方法怎么实现请求的呢?
-
Axios.js中现在的Axios.prototype.request在进行修改过,作者为了避免拦截器可能会出现异步情况或有很长的宏任务执行,并且重构之前的代码中,因为请求事放到微任务中执行的,微任务创建的时机在构建promise链之前,如果当执行到请求之前宏任务耗时比较久,或者某个请求拦截器有做异步,会导致真正的ajax请求发送时机会有一定的延迟,所以解决这个问题是很有必要的。 -
为了更好的理解pr后的改变,和改变的原因,我们先了解一下修改前的。
-
之前的Axios.prototype.request
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 || {};
}
// 合并配置 --mergeConfig.js
config = mergeConfig(this.defaults, config);
// 添加method配置, 默认为get
config.method = config.method ? config.method.toLowerCase() : 'get';
//创建用于保存请求/响应拦截函数的数组
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);
});
// 通过promise的then()串连起所有的请求拦截器/请求方法/响应拦截器
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
// 返回用来指定我们的onResolved和onRejected的promise
return promise;
};
- 整体流程:
request(config)==>dispatchRequest(config)==>xhrAdapter(config)
- 对
config判断和初始化,介于axios的调用方式不同。再通过mergeConfig函数对传入的config和this.default比较,合并配置对象中指定属性名的属性, 优先使用config中的配置。判断请求的类型方式,有就小写化,没有就默认为get。 - 创建用于保存请求/响应拦截函数的数组
chain,数组的中间放发送请求的函数,数组的左边放请求拦截器函数(成功/失败),数组的右边放响应拦截器函数。
- 为什么chain数组需要两个元素,又为什么是undefined?
- 如上图,我们都知道axios是基于promise的http库,
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。chain数组两个元素就表示fulfilled和rejected,成功就调用dispatchRequest、失败执行undefined。- 这里的
undefined没有什么具体意义,是一个占位符,当请求拦截器失败后会执行到这里的undefined,promise具有异常穿透,当使用 promise 的 then 链式调用时, 可以在最后指定失败的回调,前面任何操作出了异常, 都会传到最后失败的回调中处理。所以chain中的undefined会跳转到响应拦截器的失败回调。
- 具体实现会在下面拦截器说明
- 发送请求的函数为
dispatchRequest,发送流程dispatchRequest.js->default.js中getDefaultAdapter()->adapters.js中xhr.js或http.js,xhr.js使用是传统的XMLHttpRequest对象发起请求,http.js使用的是node模块http. - 然后通过promise的then()串连起所有的请求拦截器/请求方法/响应拦截器,返回最后的promise。
pr后的Axios.prototype.request
- 具体可以看一下下图的pr, 以及axios源码上pr讨论过程由于aixos内部promise,请求意外失败,pr参考最全、最详细Axios源码解读---看这一篇就足够了
读完个人认为: 原因是在执行下面这行代码的时候,通过前面的拦截器(成功)后,当执行
dispatchRequser函数进行xhr请求的时候,在请求没有转为promise之前,while循环还在向前运行,会占用主线程,导致axios请求延迟发送,响应页面延迟呈现,影响跳出率/转化率和用户体验。并不是每个人都利用拦截器,并且仅在使用它们时创建承诺可能是解决此问题的好方法,看pr代码:将while循环拆开了,响应拦截没有变,将请求拦截进行判断,因为用户提供的自定义适配器函数不能保证返回一个
Promise,如果用户设置的拦截器函数是异步的,就会通过interceptor.synchronous参数设置synchronousRequestInterceptors为false,进行没pr前的拦截器链式调用。如果用户设置的拦截器函数是同步的,则synchronousRequestInterceptors默认为true(即默认是同步),没有修改,然后将dispatchRequest函数从while循环中抽出,先执行拦截器,等待所有拦截器完成后调用dispatchRequest进行xhr请求,等待请求结束后在进行响应拦截器运行。
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
模拟请求
由于修改后的Axios.prototype.request复杂和难度较高(
写不来),但其核心链式调用还是不变的,所有我只能模拟修改前的请求。
function Axios(config){
this.config = config
}
Axios.prototype.request = function(config){
// 发送请求
// ...完善 合并config
// 创建一个promise 对象
let promise = Promise.resolve(config)
// 声明一个数组
let chains = [dispatchRequest, undefined] // underfined 占位
// 调用then方法指定回调
let result = promise.then(chains[0], chains[1])
return result
}
// 2. dispatchRequest
function dispatchRequest(config){
// ...一系列操作
// 调用适配器发送请求
return xhrAdapter(config).then(response =>{
// ... 对响应的结果进行转换处理
return response
},error =>{
throw error
})
}
// 3. adapter 适配器
function xhrAdapter(config){
console.log("xhrAdapter 函数执行");
return new Promise((resolve, reject)=>{
// 发送ajax请求
let xhr = new XMLHttpRequest()
// 初始化
xhr.open(config.method, config.url)
// 发送
xhr.send()
//绑定事件
xhr.onreadystatechange = ()=>{
if(xhr.readyState === 4){
if(xhr.status >= 200 && xhr.status <300){
//成功
resolve({
config: config,
data: xhr.response,
headers: xhr.getAllResponseHeaders(),
request: xhr,
status: xhr.status,
statusText: xhr.statusText
})
}else{
// 失败
reject(new Error("请求失败, 失败的请求码是"+ xhr.status))
}
}
}
})
}
let axios = Axios.prototype.request.bind(null)
// 使用axios函数发现请求, 实际上是调用 Axios.prototype.requsert
axios({
method:"GET",
url:"http://localhost:3000/posts"
}).then(response =>{
console.dir(response);
})
xhr请求只写了成功返回。
拦截器原理
- Axios.prototype.request在前面已经说了,下面是InterceptorManager.js,
function InterceptorManager() {
this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected,
// pr后的两个属性,用户设置拦截器同步或异步, 默认是同步
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) {
if (h !== null) {
fn(h);
}
});
};
function Axios(instanceConfig) {
// 将指定的config, 保存为defaults属性
this.defaults = instanceConfig;
// 将包含请求/响应拦截器管理器的对象保存为interceptors属性
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
interceptors上有request、response两个属性,两个属性都是InterceptorManager的实例,实例对象上有一个headers数组、原型对象上有一个use方法,通过use方法将用户定义的请求拦截器的成功和失败的两个回调压入到headers数组头部,如果有多个请求拦截器,每一个拦截器在上一个拦截器前面,类似先进后出。响应拦截器同样也是通过use方法添加,但他采用类似先进先出,每次都压入数组尾部。
- 当请求来到请求拦截器时,会将数组中前两个元素取出(成功回调和失败回调),处理参数返回的promise状态是成功,执行成功回调,状态是失败就执行失败回调,我们可以在拦截器回调中判断请求是否满足条件,满足放行,进行下一个拦截器
成功回调或dispatchRequset,失败进行下一个拦截器失败回调或undefined。成功回调成功执行下一个成功回调,失败执行下一个失败回调。 - 响应拦截器同样也是,不过最终返回给请求axios().then()处理结果。 流程: 请求拦截器2 => 请求拦截器 1 => 发ajax请求 => 响应拦截器1 => 响 应拦截器 2 => 请求的回调
拦截器模拟
//构造函数
function Axios(config) {
this.config = config
this.interceptors = {
response: new InterceptorManger(),
request: new InterceptorManger()
}
}
Axios.prototype.request = function (config) {
let promise = Promise.resolve(config)
const chains = [dispatchRequest, undefined]
//请求拦截器, 将请求拦截器的回调压入到chains的前面
this.interceptors.request.handers.forEach(item =>{
chains.unshift(item.fulfiled, item.reject)
})
//响应拦截器, 将响应拦截器的回调压入到chains的后面
this.interceptors.response.handers.forEach(item => {
chains.push(item.fulfiled, item.reject)
})
// 遍历
while(chains.length>0){
promise = promise.then(chains.shift(), chains.shift())
}
return promise
}
function dispatchRequest(){
// 返回一个promise对象
return new Promise((resolve, reject) => {
resolve({
status: 200,
statusText: "OK"
})
})
}
// 拦截器管理器构造函数
function InterceptorManger() {
this.handers = []
}
InterceptorManger.prototype.use = function (fulfiled, reject) {
this.handers.push({
fulfiled,
reject
})
}
// 创建axios函数
let context = new Axios()
let axios = Axios.prototype.request.bind(context)
Object.keys(context).forEach(key =>{
axios[key] = context[key]
})
//下面是测试代码
//设置请求拦截器
axios.interceptors.request.use(function one(config) {
console.log("请求拦截器 成功 -1号");
config.params = { a: 100 }
return config
}, function (error) {
console.log("请求拦截器 失败 -1号");
return Promise.reject(error)
})
axios.interceptors.request.use(function two(config) {
console.log("请求拦截器 成功 -2号");
config.timeout = 2000
return config
}, function (error) {
console.log("请求拦截器 失败 -2号");
return Promise.reject(error)
})
//设置响应拦截器
axios.interceptors.response.use(function one(response) {
console.log("响应拦截器 成功 -1号");
return response
}, function (error) {
console.log("响应拦截器 失败 -1号");
return Promise.reject(error)
})
axios.interceptors.response.use(function two(response) {
console.log("响应拦截器 成功 -2号");
return response
}, function (error) {
console.log("响应拦截器 失败 -2号");
return Promise.reject(error)
})
//发送请求
axios({
method: "GET",
url:"http://localhost:3000/posts"
}).then(response => {
console.log(response);
})
- 默认执行成功回调
取消请求原理
- 请求取消需要在config中传入一个cancelToken属性,该属性是一个CancelToken实例对象(axios.CancelToken)
axios({
method: "GET",
url: "http://localhost:3000/posts",
cancelToken: new CancelToken(function (c) {
cancel = c
})
})
- CancelToken.js中CancelToken构造函数
// CancelToken构造函数
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;
});
// 保存当前token对象
var token = this;
// 立即执行接收的执行器函数, 并传入用于取消请求的cancel函数
executor(function cancel(message) {
// 如果token中有reason了, 说明请求已取消
if (token.reason) {
// Cancellation has already been requested
return;
}
// 将token的reason指定为一个Cancel对象
token.reason = new Cancel(message);
// 将取消请求的promise指定为成功, 值为reason
resolvePromise(token.reason);
});
}
- xhr.js
// 如果配置了cancelToken
if (config.cancelToken) {
// 指定用于中断请求的回调函数
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
// 中断请求
request.abort();
// 让请求的promise失败
reject(cancel);
// Clean up request
request = null;
});
}
- 取消请求的核心是 xhr.abort()方法,所以需要明白什么时候调用就行。
- CancelToken构造函数中,给CancelToken对象实例上添加了一个promise属性,该属性是一个promise对象,并且将该对象的改变状态的
resolve方法赋值给一个变量resolvePromise。executor函数是用户存入的config中CancelToken属性的CancelToken实例对象传入的回调函数function(c){...},而executor()中的函数赋值给cancel,CancelToken实例对象添加一个reason属性(boolean)。 - 在
dispatchRequest.js调用
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
module.exports = function dispatchRequest(config) {
/*
如果请求已经被取消, 直接抛出异常
*/
throwIfCancellationRequested(config);
...
}
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
- 然后通过用户点击按钮之类的取消请求执行cancel函数,然后改变了CancelToken对象实例promise属性的状态,状态改变
xhr.js中config.cancelToken.promise.then()执行第一个参数-回调函数,结果request.abort()执行,请求取消。
模拟请求取消
- 在模拟前需要重新起到json-server,在命令行中输入
json-server --w db.json -d 2000,让请求延时2秒,否则请求成功太快,取消来不及
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="container">
<h2 class="page-header">axios取消请求</h2>
<button class="btn btn-primary">发送请求</button>
<button class="btn btn-warning">取消请求</button>
</div>
<script>
// 构造函数
function Axios(config) {
this.config = config
}
// 原型 request 方法
Axios.prototype.request = function (config) {
return dispatchRequest(config)
}
// dispatchRequest函数
function dispatchRequest(config) {
return xhrAdapter(config)
}
// xhrAdapter函數
function xhrAdapter(config) {
// 发送AJAX请求
return new Promise((resolve, reject) => {
// 实例化对象
let xhr = new XMLHttpRequest()
// 初始化
xhr.open(config.method, config.url)
// 发送
xhr.send()
// 处理结果
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
// 判断结果
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
status: xhr.status,
statusText: xhr.statusText
})
} else {
reject(new Error("请求失败"))
}
}
}
// 取消请求处理
if (config.cancelToken) {
// 对cancelToken 对象身上的promise对象指定成功的回调
config.cancelToken.promise.then(value => {
xhr.abort()
reject(new Error("请求已经取消"))
})
}
})
}
// CancelToken 构造函数
function CancelToken(executor) {
var resolvePromise;
// 为实例对象添加属性
this.promise = new Promise((resolve) => {
resolvePromise = resolve
})
// 调用executor
executor(function () {
resolvePromise()
})
}
//以下是试验
let context = new Axios()
let axios = Axios.prototype.request.bind(context)
let cancel = null
const btns = document.querySelectorAll("button")
btns[0].onclick = function () {
axios({
method: "GET",
url: "http://localhost:3000/posts",
cancelToken: new CancelToken(function (c) {
cancel = c
})
}). then(response => {
console.log(response);
cencel = null
})
}
btns[1].onclick = function () {
cancel()
}
</script>
</body>
</html>
总结
看axios源码我体会颇深,看着一行行代码,一个个不认识的单词,多个文件转来转去,真让人头大,但静下心来看,确实能学习到很多,不仅仅是源码内容,还有的是作者各种聪明的设计,例如拦截器的设计,promise链式骨架,使用数组进行压入压出,浅显易懂。例如取消请求中创建一个promise对象属性,通过对象状态的改变来判断是否取消请求。还有...
本文对axios基本功能的解读和模拟,如果对你有用,希望你能动下小手点个赞,也欢迎各位评论区指正。