这里不做axios使用的讨论,因为axios相信大家平时用的也比较多,我就没有对实际应用做过多的讨论,主要放在了axios常见的几个重要问题以及功能实现中比较巧妙地实现思路以及方法。
是一个基于promise的HTTP客户端,可以在node.js和游览器种运行。在游览器端可以向服务端发起AJAX请求,在nodejs中向远端服务发起http请求。
可以在请求前和请求结果回来在预处理,能够以promise形式书写
axiso工作原理:
游览器中,在request中发送ajax请求,创建XMLHttpRequest实例对象发送网络请求
首先,axios有请求拦截器(request)、响应拦截器(response)、axios自定义回调处理(这里就是我们常用的地方,会将成功和失败的回调函数写在这里)
//请求拦截器
axios.interceptors.request.use(function(request){
//请求成功的拦截
return request
}),function(error){
return Promise.reject(error)
}
//响应拦截器
axios.interceptors.response.use(function(response){
//请求成功的拦截
return response
}),function(error){
return Promise.reject(error)
}
假设我们定义了 请求拦截器1号(r1)、请求拦截器2号(r2)、响应拦截器1号(s1)、响应拦截器2号(s2)、自定义回调处理函数(my)
那么执行结果是:r2 r1 s1 s2 my
由此就产生第一个问题,也就是为什么请求拦截器是先打印后处理的而响应则是顺序执行呢?
在axios源码中,有一个叫做chain的数组,他说拦截器的中间件,如下图,这里的dispatchRequest就是成功的时候会执行的函数,说白了就是成功的时候需要执行的ajax的请求
var chain = [dispatchRequest,undefined]
右边的undefined是可以理解成占位符,因为既然有成功执行的函数,那么就有失败需要执行的,失败则执行undefined(这里比较有意思,promise的异常会穿透,当前面有失败时候会执行到undefined,然后透过undefined继续执行之后的失败)。
当我们写了请求拦截器函数的时候,请求函数会被循环遍历,将拦截器的成功回调和失败回调
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor){
// 将请求拦截器压入数组的最前面
chain.unshift(interceptor.fulfilled,interceptor.rejected)
})
成对压入chain数组中,这里的成对是一个关键点,从代码处可以看出请求拦截器向chain中压入的时候使用的是unshift方法,也就是每次添加函数方法队都是从数组最前面添加,这也是为什么请求拦截器输出的时候是r2 r1。
当我们写了响应拦截器的时候,和请求拦截器一样会将拦截器的成功和失败回调压入chain数组之中,如下图,会将响应拦截器函数依次循环遍历压入数组最尾部,使用的是push方法,与
this.interceptors.response.forEach(function unshiftRequestInterceptors(interceptor){
// 将响应拦截器压入数组的最尾部
chain.push(interceptor.fulfilled,interceptor.rejected)
})
unshift不同的是,push新增函数一直被push到最尾部,那么形成的就是s1 s2的顺序,这也就解释响应拦截器函数是顺序执行的了。
axios.interceptors.response.use和axios.interceptors.request.use在定义响应和请求拦截器的时候只是将成功和失败的回调放在了response和request的hander的里面,以回调函数的形式存储起来,最后axios函数调用的时候通过push和unshift分别从队尾和队首加入到chain数组中去,最后通过循环遍历执行chain数组中的回调函数,完成请求拦截、请求执行、响应拦截这一系列过程。
这里chain数组有点像Vue中的Dep类的作用,Dep的bucket收集依赖桶中就是收集当数据发生变化的时候会执行的回调函数,chain是添加成功和失败的回调函数对,这是为什么之前红字标识成对很关键,当成功的时候按照黑线依次执行下去,如果遇到异常或者错误会转而执行红线,这里值得注意的是如果是执行到第二个R2的时候出现异常,那么接下来都是以红线路径执行下去了。
作用是将决定实例cancelToken的promise状态的resolvePromise变量暴露出来,让外部能够执行resolvePromise的变量从而改变promise的状态为成功,如果不做任何改变处理的话,实例cancelToken中的promise始终处于pending
的状态。
function CancelToken(executor){
// 声明一个变量
var resolvePromise
// 实例对象身上添加promise 属性
this.promise = new Promise(function promiseExecutor(resolve){
// 将修改promise对象成功的状态暴露出去,这里的话只需要执行resolvePromise()
// 就相当于执行了resolve()方法也就是成功的回调
resolvePromise = resolve
})
// 将修改promise状态的函数暴露出去,通过cancle = c可以将函数赋值给cancel
// 这样执行cancle就相当于执行内部的resolvePromise函数也就是实例promise中的resolve
executor(function (){
resolvePromise()
})
}
作用是真正去取消一个ajax请求,真正意义上的取消请求,将这个函数包装在了一个cancelToken属性的promise成功的回调函数中,也就是说在axios的配置中首先要有cancelToken这个属性才能够取消发送请求,而取消发送请求放在了cancelToken的promise的成功的回调执行中。
// 如果配置了cancel则调用then方法设置成功的回调
if(config.cancelToken){
// 将取消请求的函数放在了一个cancelToken的promise的成功回调之中,不一定会执行
// 如果cancelToken的promise执行成功回调就会执行下列代码
config.cancelToken.promise.then(function onCanceled(cancel){
if(!request){
return
}
// 真正取消请求的函数request.abort()
request.abort()
reject(cancel)
request = null
})
}
// 声明全局变量
let cancel = null
// 发送请求
btns[0].onclick = function(){
// 检测上一次请求是否已经完成
if(cancel !== null){
// 取消上一次请求
cancel()
}
// 创建cancelToken的值
let cancelToken = new axios.CancelToken(function(c){
// 将c的值赋值给cancel
cancel = c
})
axios({
method:'GET',
url:'http://localhost:3000/posts',
// 需要配置这个属性才能够取消请求
cancelToken:cancelToken
}).then(response=>{
// 请求结束的时候,将全局变量cancel初始化
cancel = null
})
}
// 绑定第二个事件取消请求
btns[1].onclick = function(){
// 执行cancel函数
cancel()
}
实现步骤及原理:
这里的实现非常巧妙,我本来不是很想写这篇文章,但是这个地方吸引到我了,我觉得还是蛮有意思的,开始有介绍到axios是基于promise的,这里CancelToken函数会实例一个promise,这个promise的状态会受到一个CancelToken函数中的全局变量resolvePromise的影响,resolvePromise变量是resolve引用类型,也就是说执行resolvePromise相当于执行了resolve()函数也就是相当于将CancelToken中promise由pending改变成了resolve也就是成功的回调,resolvePromise函数是通过实例中的executor函数中执行的,当我们去实例一个cancelToken的时候也就是new的时候CancelToken函数就会执行,执行就会生成一个promise,promise的状态由resolvePromise决定,执行完promise后执行executor,executor的实参就是红色圈内的函数,那么红色圈内的函数会执行,红色圈内的函数执行,那么executor函数中的蓝色function就会作为参数c,那么cancel = c就是将蓝色圈内函数赋值给cancel,那么cancel执行 c就会执行,c执行,resolvePromise()就会执行,resolvePromise()执行resolve()就会执行,那么promise的状态就确定为resolve了,这里比较难理解(红蓝圈,这样就是实参和形参有点混乱),
蓝红圈详解:
function CancelToken(executor){
// 声明一个变量
var resolvePromise
// 实例对象身上添加promise 属性
this.promise = new Promise(function promiseExecutor(resolve){
// 将修改promise对象成功的状态暴露出去,这里的话只需要执行resolvePromise()
// 就相当于执行了resolve()方法也就是成功的回调
resolvePromise = resolve
})
// 将修改promise状态的函数暴露出去,通过cancle = c可以将函数赋值给cancel
// 这样执行cancle就相当于执行内部的resolvePromise函数也就是实例promise中的resolve
// executor内部的方法其实是作为参数 相当于以下代码:
var c = function(){
resolvePromise()
}
executor(c)
// 蓝色圈的函数其实是executor的一个参数,而这里executor其实又是一个形参
// executor其实是红色圈内的函数
excutor = function (c){
cancel = c
}
// 结合上述就等于
// 前面为了方便理解已经说明了c是什么
c = function(){
resolvePromise()
}
function 真正执行的函数(c){
cancel = c
}
// 前面的形式都是为了方便理解的过度形式
// 实际执行executor函数就是下列代码
function 真正执行的函数(function c(){
resolvePromise()
}){
cancel = function c(){
resolvePromise()
}
}
//这里需要理解实参和形参才不容易搞混,c是形参 实参是executor的内部function函数,executor在CancelToken处其实是形参,实参是传入CancelToken的那个函数function(c)函数。
}
这里需要理解实参和形参才不容易搞混,c对于function(c)是形参 他的实参是executor的内部function函数,executor对于CancelToken()是形参,实参实例时候传入CancelToken的那个函数,例如这里的function(c)函数。
axios能够通过axios.get({})等方式调用,是因为axios的原型上写有这些方法,根据原型链我们知道,实例出来的axios如果没有这些方法会沿着原型链找下去,而get put post这些方法就写在原型__proto__上,就像数组的push方法这些一样,能够在原型上找到。
那么axios为什么能够axios({method:'GET'})这样当函数去使用呢?因为instance函数的作用,如下图,context创建一个原型后就能沿着原型链找下去,这就是为什么能够axios.get() axios.post()调用,因为在创建Axios原型的时候除了default和interceptors方法以外,还为原型链添加get、post等等一系列方法,调用的时候沿着Axios对象的原型链找下去即可调用,而能够通过
function createInstance(config) {
// 实例化一个对象,通过axios.get()等方法能够调用就是因为
// 方法写在了Axios对象的原型上,所以可以调用
let context = new Axios(config)
// 创建请求函数 instance是一个函数并且可以通过instance({})使用,但是还没有方法
let instance = Axios.prototype.request.bind(context)
// 将Axios.prototype对象中的方法添加到instance函数对象中
Object.keys(Axios.prototype).forEach(key =>{
instance[key] = Axios.prototype[key].bind(context)
})
// 除了拷贝原型对象最底层上的方法,Axios原型上的方法也需要拷贝下来
// 为instance函数对象添加属性default与interceptors
Object.keys(context).forEach(key =>{
instance[key] = context[key]
})
return instance
}
let axios = createInstance()
// 发送请求
axios({method:'GET'})
函数的方式调用就依赖于instance函数他将Axios对象原型上的方法拷贝到自身了,最后通过return instance返回给axios实例,这样axios实例中也就有method等参数了,也就能够通过函数的方法调用了,因为这是将Axios原型对象最底层的方法引用了,道理是一样的,同时不要忘了将Axios对象本身就有的default和interceptors(拦截器)方法也添加,这样都可以通过axios({})函数方式配置和调用了。