axios部分工作原理及常见重要问题的探析:

  • Post author:
  • Post category:其他


背景:

这里不做axios使用的讨论,因为axios相信大家平时用的也比较多,我就没有对实际应用做过多的讨论,主要放在了axios常见的几个重要问题以及功能实现中比较巧妙地实现思路以及方法。

一、axios:

是一个基于promise的HTTP客户端,可以在node.js和游览器种运行。在游览器端可以向服务端发起AJAX请求,在nodejs中向远端服务发起http请求。

可以在请求前和请求结果回来在预处理,能够以promise形式书写

axiso工作原理:

游览器中,在request中发送ajax请求,创建XMLHttpRequest实例对象发送网络请求

二、常见重要问题:

1.axios拦截器工作原理:

背景:

首先,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继续执行之后的失败)。

1.1请求拦截器

当我们写了请求拦截器函数的时候,请求函数会被循环遍历,将拦截器的成功回调和失败回调

this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor){
    // 将请求拦截器压入数组的最前面
    chain.unshift(interceptor.fulfilled,interceptor.rejected)
})

成对压入chain数组中,


这里的成对是一个关键点


,从代码处可以看出请求拦截器向chain中压入的时候使用的是unshift方法,也就是每次添加函数方法队都是从数组最前面添加,这也是为什么请求拦截器输出的时候是r2 r1。

1.2响应拦截器

当我们写了响应拦截器的时候,和请求拦截器一样会将拦截器的成功和失败回调压入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的时候出现异常,那么接下来都是以红线路径执行下去了。

2.axios取消请求原理:

CancelToken函数:

作用是将决定实例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()
    })
}

request.abort()函数

作用是真正去取消一个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
    })
}

Cancel的声明以及取消的请求的实际配置:

// 声明全局变量
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)函数。

3.为什么axios既可以当函数调用,也可以当对象使用:

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({})函数方式配置和调用了。



版权声明:本文为weixin_54515240原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。