axios的拦截器是如何实现promise异步队列的

背景描述

假设我们现在需要实现一个函数,函数传入一个函数队列和一个初始参数,要求将初始参数依次传入队列中的每一个处理函数,函数的返回值为下一个函数接受的参数,最后一个函数的返回值作为最终的返回值。

这类问题有哪些解决思路呢?

首先考虑如果函数队列全部都是同步的情况,这类问题相对比较简单,实现一个递归函数,递归调用即可。(具体实现方案大家也可以去试一试,这里不做深入讨论)

万一队列里的函数都是异步函数呢?或者甚至存在异步函数和同步函数混合使用的情况,那该如何处理呢?

接触过 express 中间件的小伙伴都知道,express 的中间件就是一个异步函数的队列,通过调用每个函数的 next 参数从而执行下一个中间件的(具体实现方式可以去看 express 的源码 ,这里也不做深入讨论),这种实现方式就强制将每个处理函数都当做了异步函数来处理,有时候我们不小心忘了在某个条件分支调用next,就会导致请求卡死无法响应的情况。

express 团队在新开发 koa2 中改成了 promise 的形式,并采用洋葱模型,是一种更好的解决方案( Github 地址

而 axios 的拦截器处理方式则更加简单粗暴,我们今天着重分析一下它的实现方式。

源码解析

直接上源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// dispatchRequest:调用接口的逻辑
let chain = [dispatchRequest, undefined];

// 先注册的后执行,保证配置优先级最高
interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

// 先注册的先执行,保证处理响应的顺序一致
interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

// 最开始传入的config
let promise = Promise.resolve({
text: "this is config",
});

// 依次执行队列里的异步函数
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}

// 最终业务拿到的数据
promise.then((data) => {
console.log("response:", data);
});

核心逻辑 1:拦截器队列构造(1~12 行)

  1. 队列中每两项为一组,第一项为 resolve 的回调,第二项为 reject 的回调
  2. 初始队列为发起请求的逻辑,这个函数直接返回请求的 promise,第二个 reject 的回调为空,因为这里并不需要处理上个异步函数的错误(理论上出错了就不应该出现在这里,直接跳出到业务处理函数即可)
  3. request 的拦截器使用 unshift 推进队列,所有的拦截器都在请求前执行,先注册的函数后执行,保证先配置的优先级最高,避免被后面的拦截器覆盖,并将最终处理完的 config 传给请求函数
  4. response 的拦截器使用 push 推进队列,接受请求函数传过来的初始返回值,先注册的函数先执行,保证响应体的处理顺序一致,最终将处理完的响应体传到业务层

核心逻辑 2:队列的执行(14~22 行)

个人觉得这段代码非常精彩,需要对异步函数和同步函数的概念非常熟悉才能读懂

  1. 原始的 promise 将初始化的配置传到 promise.then 的回调中去
  2. 通过 chain.shift 将队列中的第一个处理函数出栈并传入 promise.then 的参数,此时返回了一个新的 promise,他是这个处理函数返回的 promise 对象(这是同步操作)
  3. 此时 chain.length 同步-1,队列中还有其他函数,继续执行循环,注意这个时候 promise 变量已经变成了第一个回调函数的返回值(但此时这个异步函数本身并没有 resolve)
  4. 然后会继续将队列中的函数出栈,将其返回值赋值给 promise 变量
  5. 执行循环直至所有的异步函数都出列

注意以上的逻辑全部都是同步执行的,都还没有进入到数据的处理阶段,我们可以将循环后的逻辑用以下代码直观表达:

1
2
3
4
5
6
7
8
9
Promise.resolve({
text: "this is config",
})
.then(chain[0], chain[1])
.then(chain[2], chain[3]) // 以上都是request的拦截器
.then(dispatchRequest, undefined) // 这里发起请求
.then(chain[6], chain[7]) // 以下都是responce的拦截器
// ...
.then(chain[n - 2], chain[n - 1]); // n = chain.length

这样看就更符合我们的直观感受。而 promise 队列的写法将其变得非常简洁且自由,配合注册函数可以同时注册多个回调函数且按照注册事件的顺序执行,我们在封装一些公共方法的时候也可以借鉴这个写法。

核心逻辑 3:关于错误处理

以下是拦截器的注册逻辑:

1
2
3
4
5
6
7
8
9
10
interceptors.request.use(
(config) => {
console.log("config2:", config);
return config;
},
(error) => {
console.log(error);
return Promise.reject(error);
}
);
  1. 第一个参数是 resolve 拦截器,第二个参数是 reject 拦截器
  2. resolve 拦截器可以直接 return 数据,在 promise 回调中会自动将其作为一个立即 resolve 的 promise 来处理,当然也可以直接写return Promise.resolve(data)
  3. reject 拦截器建议返回一个 reject 的 promise,这样代码会直接跳到下一个拦截器的错误处理。否则会进入下个 resolve 的处理函数中,导致报错,可以用如下代码测试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Promise.reject(new Error("错误1"))
.then(
(res) => {},
(err) => {
console.log(111, err);
return Promise.reject(err);
}
)
.then(
(res) => {},
(err) => {
console.log(222, err);
}
)
.then((res) => {
console.log("resolve");
})
.catch((err) => {
console.log(333, err);
});

axios的拦截器是如何实现promise异步队列的
https://www.wobushi.top/2020/axios的拦截器是如何实现promise异步队列的/
作者
Pride Su
发布于
2020年12月22日
许可协议