最近在做axios的二次封装,在配置拦截器时。发现实际的调用流程与预想的不太一致。所以去看了看axios拦截器部分的源码,大概了解拦截器的实现。 一下是对拦截器实现的一些理解。
拦截器的使用方式
// 请求拦截
axios.interceptors.request.use(
// 处理器
function onFulfilled (){...},
// 错误捕获
function onRejected (){...},
)
// 响应拦截器
axios.interceptors.response.use(
// 处理器
function onFulfilled (){...},
// 错误捕获
function onRejected (){...},
)
一个简单例子
const c = axios.create({
baseURL: '/proxy',
timeout: 1000,
})
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
function req1(conf: AxiosRequestConfig){
console.log('r1')
return conf
}
function req2(conf: AxiosRequestConfig){
console.log('r2')
throw new Error('from r2')
}
function req3(conf: AxiosRequestConfig){
console.log('r3')
return conf
}
function err1(){
console.log('e1')
}
function err2(){
console.log('e2')
}
function err3(){
console.log('e3')
}
c.interceptors.request.use(req1, err1)
c.interceptors.request.use(req2, err2)
c.interceptors.request.use(req3, err3)
c.get('/', {cancelToken: source.token})
.then(() => console.log('req end'))
.catch((e) => console.log('err end', e))
// r3
// r2
// e1
// err end
因为平常一直使用
promise.then(success).catch(fail)
的模式,潜意识认为axios拦截器的流程也类似, 而实际调用的结果与预期不一致, 预期调用流程: r1 → r2 → e2。 那拦截器真是的调用流程是什么样的呢?
拦截器实现
axios 拦截器相关的代码主要在,lib/core/Axios.js
lib/core/InterceptorManager.js
两个文件中。
注册拦截器
请求和响应拦截器都是
InterceptorManager
的实例。所以两者的注册方式是一致的
InterceptorManager 拦截器对象
'use strict';
var utils = require('./../utils');
function InterceptorManager() {
// 执行函数缓存队列
this.handlers = [];
}
/**
* fulfilled, rejected 将交由Promise.then函数
* 返回注册id,以便移除对映的拦截器
*/
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
});
// 返回的id为队列的长度
return this.handlers.length - 1;
};
/**
* 通过id移除拦截器
*/
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
// 应为拦截器的标识id,为队列的长度
// 而 handlers.length 是动态的
// 为了防止id重复,删除拦截器时,将对应的位置置空,而不是删除
// 保证length的值一直处于递增的状态
this.handlers[id] = null;
}
};
/**
* 遍历拦截器队列
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
// 跳过空值,既已删除位
if (h !== null) {
fn(h);
}
});
};
module.exports = InterceptorManager;
- 通过源码可以看到,拦截器实例实现很简单。主要是维护一个对应的队列。
2. synchronous
runWhen
配置项只在项目README中有说明,当部分中文文档中没有提及,后面Axios源码中能了解实际的用途。
- 需要的注意的是,
use
函数返回的绑定id,为队列的长度。所以不要直接通过InterceptorManager 实例修改拦截器队列
拦截器调用流程
拦截器调用流程的代码都在 Axios.prototype.request
方法中
- 收集请求拦截
// Axios.js
// 请求拦截收集队列
var requestInterceptorChain = [];
// 是否存在同步配置
var synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 判断是否存在runWhen函数,通过runWhen执行返回,判断是否跳过当前拦截器
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
// 收集同步状态设置
// 必须要求所有可执行拦截器,都配置 synchronous 时。
// synchronousRequestInterceptors 最终值才能为true,执行同步调用模式
// 否则为false, 将执行异步调用模式
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
// 收集拦截器
// 这里添加模式为unshift
// 所以最终队列顺序与注册顺序相反
// 例如:注册顺序:[1, 2, 3] 收集器顺序:[3, 2, 1]
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
- 收集响应拦截
// 响应拦截收集队列
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
// 与请求拦截不同,这里没有对 runWhen,synchronous 的判断
// 所以两个配置只作用于请求拦截
// 这里添加的模式 push
// 所以最终的队列顺序与注册顺序一致
// 例如:注册顺序:[1, 2, 3] 收集器顺序:[1, 2, 3]
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
- 异步调用模式
var promise;
// 拦截器的两种调用模式
// 当所用拦截器都为配置,synchronous 属性时,使用异步队列(默认模式)
if (!synchronousRequestInterceptors) {
// chain为请求任务执行队列
// dispatchRequest 请求发送器
// 因为队列任务将通过Promise.then(task, error) 模式调用
// 所以默认队列包含 一个 undefined 值,作为发送器的错误捕获器占位符
// Promise.then(dispatchRequest, undefined)
var chain = [dispatchRequest, undefined];
// 将请求拦截追加到队列头部
Array.prototype.unshift.apply(chain, requestInterceptorChain);
// 将响应拦截追加到请求发送器之后
chain = chain.concat(responseInterceptorChain);
// 最终的任务队列顺序
// 反序的请求拦截 -> 请求发送 -> 正序的响应拦截
promise = Promise.resolve(config);
// 执行任务队列
while (chain.length) {
// 每个任务都是由 执行器,错误捕获成对执行的
// 所以初始队列包含一个undefined占位符
promise = promise.then(chain.shift(), chain.shift());
}
// 返回promise
return promise;
}
// 当 synchronousRequestInterceptors 为true时,启动同步模式
var newConfig = 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);
}
// 执行响应队列
// 响应队列的执行与异步模式一致,所以在收集任务时,没有做 synchronous 判断
while (responseInterceptorChain.length) {
promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
}
return promise;
小结
通过阅读源码,我们能大概梳理出拦截器的大致执行流程和特点
- 请求拦截存在
异步
同步
两种模式 - 请求拦截(反序)和响应拦截(正序)的执行顺序与注册顺序不同
- 只有当所有请求拦截都开启同步模式时,才执行同步模式, 否者依然使用异步模式
- 请求拦截可根据情况跳过,而响应拦截不具备该功能
- 不要直接通过拦截对象修改拦截器队列
- 请求拦截器需要将最终的处理结果交给发送器执行, 所以必须保证最有执行的请求拦截有正确返回
异步,同步模式的执行差异
两例子说明二者的差异
- 异步
function req1(){
console.log('r1')
}
function req2(){
console.log('r2')
}
function req3(){
console.log('r3')
throw new Error('from r3')
}
function err1(){
console.log('e1')
}
function err2(){
console.log('e2')
}
function err3(){
console.log('e3')
}
const p = Promise.resolve()
p
.then(req3, err3)
.then(req2, err2)
.then(req1, err1)
// r3 -> e2 -> r1
- 同步
const task = [
[req3, err3],
[req2, err2],
[req1, err1]
]
for(let[req, err] of task){
try {
req()
} catch (error) {
err()
break
}
}
// r3 -> e3
比较连个例子可以发现,两种模式主要的区别在于错误的处理上
- 异步模式的错误处理类似分支,错误捕获的是之前节点最近的一次错误
- 同步模式的错误处理针对与当前执行函数
then(success, fail) 与 then(success).catch(fail)
p
.then(req3)
.catch(err3)
.then(req2)
.catch(err2)
.then(req1)
.catch(err1)
// r3 -> e3 -> r2 -> r1
异步任务是以 then(success, fail)
的方式调用的,错误捕获的节点与then(success).catch(fail)
是不同的,promise错误捕获的方式是根据当前promise节点的状态来判断的,第二中方式比第一种方式,中间会多出一个节点。
所以在配置错误处理回调时,需要注意处理的节点位置。