axios 拦截器源码简单分析
下文拉取分析 axios 库的时间点为2021-11-29
项目仓库地址:https://gitee.com/classlate/rec-axios_interceptor第一次尝试分析源码,所以前半部分记录了几乎全部的视角,有点啰嗦,后面针对拦截器的解读,意思可能表达到了,但语言上面没有做过多的精炼,白话了一点
克隆 axios 库
git clone git@github.com:axios/axios.git
下文提到的文件地址全部为依据 axios 库根地址的相对路径
在寻找拦截器时可以简单看一下 axios 的大概执行流程,和日常使用情况印证一下
package.json
,通过main
的指向,得知入口文件为/index.js
package.json
/index.js
,仅一行导入且同时导出的代码/index.js
/lib/axios.js
,反向查找行54:获得当前文件的导出目标,变量axios
行34:找到变量axios
的声明位置,为一个被传入实参defaults
的函数createInstance
的执行返回接收
行33:// 导出创建默认实例以备导出
方法:createInstance
实参:defaults
这里简单看一下
defaults
defaults
的定义位置,行7,知晓实际为一个导入/lib/defaults.js
,依旧反向查找源头,行134(得知导出变量defaults
)createInstance
传入了一个对象adapter
,timeout
,validateStatus
,headers
等等行15:转到函数createInstance
的定义位置
行9-行14:
// 创建一个 Axios 实例(告诉了我们 Axios 是一个自定义构造函数)
// 接收一个对象参数,对象中包含的是实例对象的默认配置(参见上面的
default.js
)// 返回 Axios 实例对象
函数createInstance
向方法里面看 ,依旧反向摸索,行30:返回值,变量instance
行17:变量的定义位置,接收的是一个名字为bind
的函数返回值
行4:来到bind
函数定义的地方,原来是一个模块的导入,看一下
/lib/helpers/bind.js
,了解一下工具函数bind
bind
返回值的instance
)的所有参数(apply 方法)返回继续查看
bind
其中的这两个实参
Axios.prototype.request
,Axios
实例方法,根据工具函数bind
定义可以知道调用instance
(也就是实际使用中的 axios 调用)实际上就是调用这里的参数1context
→→ 行16(new Axios(defaultConfig)),一个 Axios
实例Axios
定义的地方
/lib/core/Axios.js
/lib/core/Axios.js
,简单收起来看一下最外层的逻辑,然后依旧是反向摸索,Axiso
) →→ 行16(变量Axios
的定义位置,可以看到其本身是一个方法,准确的讲是一个自定义构造函数)interceptors
,属性值为一个对象(包含了属性request
和response
,即分别对应的请求和相应拦截,属性值为一个自定义构造函数InterceptorManager
的实例)InterceptorManager
,又看到了模块/lib/core/inInterceptorManager.js
/lib/core/InterceptorManager.js
,拦截器的定义一览无余use
方法就在其中/lib/core/Axios.js
/lib/core/Axios.js
,追踪 Axios 实例方法 request,行29dispatchRequest
的执行返回值)dispatchRequest
定义的位置,一个模块导入/lib/core/dispatchRequest.js
defaults.adapter
继续反向追踪查找)defaults
模块导入axios/lib/defalts.js
defaults
) →→→ 行44(变量defaults
的声明位置)adapter
,函数getDefaultAdapter
的执行返回值getDefaultAdapter
的定义位置,大概意思能看出来是一个为不同运行环境分配不同adapter
的判断require('./adapters/xhr')
axios/lib/adapters/xhr.js
xhrAdapter
的定义位置)xhrAdapter
的返回值,一个 Promise 实例resolve
和 reject
的位置,即 Promise 的两个状态 →→→ 行66:函数settle的执行lib/core/settle.js
resolve
返回还是 reject
返回,同时在返回时将携带参数3lib/adapters/xhr.js
,考察使用该函数时,传入的参数3lib/adapters/xhr.js
response
response
定义位置settle
定义的地方(模块文件lib/core/settle.js
),通过参数2,还能够了解到针对reject
,其实是对response
又再次进行了包装,然后继续反向查找,能够在模块文件lib/core/createError.js
中了解到这次的包装情况lib/core/createError.js
createError.js
enhanceError
进行再处理enhanceError
的定义位置,依路径地址进入模块文件lib/core/enhanceError.js
enhanceError
/lib/core/inInterceptorManager.js
handlers
(数组),以及在此构造函数原型上添加了3个方法use
方法,添加一个拦截器,平时的像axios.interceptors.request.use()
这样的使用,其中的use
方法就是来自这里。可以接收3个参数(第3个可选,为一个对象,可配置属性 synchronous
和runWhen
,缺省时各为false
和null
,即假值,有个印象,拦截器执行时会用到),读取参数,分配对应的属性名后,包装为一个对象尾部添加到实例属性handlers
数组中,返回此包含了拦截器信息的对象在数组中的索引(用于移除拦截器)eject
方法,移除一个拦截器,接收拦截器在handlers
数组中的索引作为参数,将此拦截器赋值null
,之所以没有直接删除,目前推断是避免移除后会导致定义拦截器时返回的索引和实际索引不一致的问题forEach
方法,先看函数的内部代码,接收一个函数作为参数,函数体中调用了一个工具函数forEach
(传入了两个参数,1存放拦截器信息的数组,2函数(暂时给个别名为 fn,下面提到的时候可以直接联想到这里)。forEach
的定义位置,结合上面使用时传入的参数1为数组,解读目标可以直接放到行243-行246。forEach
方法,即传入拦截器信息数组,进行遍历,如果元素不为null
(对应移除拦截器的赋值为null
的处理,即拦截器未被移除),将此元素作为参数传给fn(上面4中的参数2)并立即执行。来到模块文件/lib/core/Axios.js
,即创建拦截器实例的地方,前面得知了平时类似axios(参数)
这样的使用其实就是Axios.prototype.request(参数)
,下面直接看向这个方法内部,粗略查看后 可以定位到行60-行117,拦截器的执行就在其中
然后再稍微细看一下行60-行117,从行78声明变量promise
开始,紧跟了一个if 判断,其中包含了return promise
(行91),再就是if 判断下面一直到最后行117,这里是另外一个条件下的return promise
。
由此可以大体分为分为三部分:行60-行76(拦截器执行的前期准备部分),行80-行92(一种情况下的返回),行95-行117(另一种情况下的返回,与第二部分互斥)。
第一部分:行60-行76
得益于变量名的语义化,可以轻松且提前得知变量或函数在代码 中的作用
行61:声明一个数组requestInterceptorChain
,用于链式存放请求拦截器
行62:声明一个状态位变量synchronousRequestInterceptors
,标记请求拦截器是否为同步执行
行63-行71:执行拦截器原型上面的方法forEach
,传入一个函数作为参数。根据前面对原型方法forEach
以及内部所用工具函数forEach
的分析,这个作为参数的函数unshiftRequestInterceptors
的参数为各个存放拦截器信息的对象
行64-66:读取拦截器信息对象的runWhen
属性,如果此属性为一个函数且该函数在接收请求配置作为参数后的返回值为false
,则直接return
,即跳过当前遍历的请求拦截器
以上条件不满足则继续向下执行
// 一个简单的 runWhen // 下面的声明表示如果请求方式为 POST 则跳过此条请求过滤器的处理 function jumpPost(config) { if (config.method.toUpperCase() === 'POST') { return false } return } // 使用 // ... axios.interceptors.request.use((config) => { //... return config }, (error) => { // ... return Promise.reject(error) }, { runWhen: jumpPost // 上面定义的函数 jumpPost })
行68:进行逻辑与判断,条件全部为真则synchronousRequestInterceptors
再次(声明变量时有同时赋值true
)赋值为true
,
如果拦截器信息对象中配置了属性synchronous
且值为假值,则此时synchronousRequestInterceptors
被赋值为false
// 使用 // ... axios.interceptors.request.use((config) => { //... return config }, (error) => { // ... return Promise.reject(error) }, { synchronous: false // 或者 true,缺省时也为 false })
行70:向请求过滤器链组requestInterceptorChain
头部放入定义过滤器时的两个回调函数
行73:声明一个数组responseInterceptorChain
,用于链式存放响应拦截器
行74-行76:依旧调用了拦截器原型上面的方法forEach
,处理和上面的请求拦截器相比,没有那么多前置语句,直接向响应过滤器链组responseInterceptorChain
尾部放入定义过滤器时的两个回调函数
归纳:该部分将请求拦截器依照反序(相对于定义顺序来讲,下同)、响应拦截器依照正序,各自存放在一个数组中,另外请求拦截器方面如果配置了synchronous
为true
最后使得状态位变量synchronousRequestInterceptors
为真则请求拦截器整体同步执行
第二部分:行80-行92
代码如下
行80:判断条件,状态位变量synchronousRequestInterceptors
是否为真,以决定是否执行此部分
行81:声明并赋值一个数组chain
,按顺序存放了两个元素,前面梳理axios的执行流程可知dispatchRequest
为axios发起请求的环节
行83:向数组chain
头部添加请求拦截器链组
,注意此时可不是将请求拦截器链组整个数组作为元素添加,这里通过了apply
绑定,requestInterceptorChain
这个数组会被分解传入 unshift
方法
行84:将数组chain
与响应拦截器链组进行合并
行86:创建一个携带请求配置成功返回的Promise 对象
行87-行89:开启 while 循环,条件为数组chain
中有元素时
行88:循环内部仅有的一行代码,从数组chain
头部两两取出元素(看到这里的两两取出,且联想一下内部拦截器是两两为一个存放的,那行81中的初始数组赋值就不难理解了,undefined
其实就是为了配合作为占位用的),并执行
且由于请求拦截器存入时是按照定义顺序头部依次放入,此时为头部按序取出,所以可得知请求拦截器是反向与定义顺序执行处理的
注意,由于 JS 的执行机制,Promise 会被当做微任务进行处理,即此行的.then()
会被放入异步事件列表随后进入事件队列,并非立即执行,执行的时机为主线程执行完毕,也就是说请求拦截器的处理会被主线程堵塞,当然了,数组chain
中的剩余元素在取出执行时依然如此
可如下简单测试
let promise = Promise.resolve('这是待处理的配置') let i = 3 console.log('start') while(i) { console.log(i) i-- promise = promise.then((a) => { console.log(a) return a }, e => Promise.reject(e)) } console.log('finish') // 最后的执行结果如下,你想到了吗 // start // 3 // 2 // 1 // finish // 这是待处理的配置 // 这是待处理的配置 // 这是待处理的配置
行91:返回结果(一个 Promise 对象)
归纳:该部分的重点就是在行88,由于为 Promise 对象回调的缘故,因此处理会被放入异步中,等待主线程空闲时再继续执行
第三部分:行95-行117
代码如下:
(运行此部分代码的条件为每一个请求拦截器定义时传入了第三个参数,一个含有属性synchronous
值为true
的对象)
行95:声明新的变量接收请求配置
行96:while
循环,开启条件为请求拦截器链组不为空时,
行97-行98:头部依次取出定义请求拦截器时定义的两个回调函数
这里简单写一下请求拦截器的代码
axios.interceptors.request.use( (config) => { // 在发送请求之前做些什么 return config }, (error) => { // 对请求错误做些什么 return Promise.reject(error) })
行99-行104:正常执行请求拦截器的第一个回调函数,如一旦遇到错误则执行第二个回调函数,并跳出while
的循环,取消后续请求拦截器的处理
此时遇到的错误并不会影响请求的正常发送,请求会拿着之前请求拦截器处理无误的配置继续发送请求,
注意,此处对于请求拦截器的遍历执行,并非为第二部分中的 Promise 对象,而只是一个普通的函数,因此对于请求拦截器的处理会在主线程中直接执行,这也是第二三部分最最主要的区别,也是参数对象synchronous
属性的真实用意
行107-行111:携带经请求拦截器处理过的配置发送请求拿到响应数据(上面对Axios 流程梳理时可得知执行函数dispatchRequest
的结果为一个Promise 对象,因此此处的变量promise
中存放了一个 Promise 对象),成功则跳过 catch 语句继续向下执行,有错误则进入 catch 直接返回一个失败的Promise 对象
行113-行15:在经上面发送请求成功然后得到响应数据后,如果响应拦截器链组不为空(即定义了响应拦截器),开启while 循环,从链组中头部取出响应拦截器执行,处理响应数据,然后和第二部分最后的情况一样,Promise 的回调会被推入微任务,等待主线程任务执行完毕后才会执行
行117:返回结果(一个 Promise 对象)
归纳:这个部分与第二部分最明显的差别就是,请求拦截器的处理并不会当作异步任务,会在主线程中直接进行处理