本文参加了每周一起学习200行源码共读活动
我们在使用 axios
时都或多或少用过拦截器,主要是针对不同的情况做出不同的处理。比如请求前设置 token
,响应后解析数据等等。
axios
拦截器代码在 lib/core/InterceptorManager.js
,拦截器的实现其实并不复杂。整个拦截器也是用 ES5
的写法实现了一个 类,主函数为 InterceptorManager
,话不多说,先看图:
从图中我们可以看出,拦截器的的实现很简单,一个 handlers
用来存储拦截器方法,一个 use
用来向 handlers
中增加拦截器,一个 eject
用来从 handlers
中移除拦截器,一个 forEach
用来遍历出拦截器。
InterceptorManager
function InterceptorManager() {
this.handlers = [];
}
主函数不用多说,不过需要说明一下。
use
/**
* Add a new interceptor to the stack
*
* @param {Function} fulfilled The function to handle `then` for a `Promise`
* @param {Function} rejected The function to handle `reject` for a `Promise`
*
* @return {Number} An ID used to remove interceptor later
*/
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
});
return this.handlers.length - 1;
};
这里直接将拦截器 push
进 handlers
。需要注意的是,这里与上一章中,我们看到的 promise
构建是不一样的,promise
链的原始结构 requestInterceptorChain
和 responseInterceptorChain
数组里,是 0
及偶数索引是 fulfilled
,奇数索引是 rejected
。
这里是每一个元素都包含了正常处理函数 fulfilled
、异常处理函数 rejected
、同步运行属性 synchronous
、运行时间判定函数 runWhen
。
这块之所以是这样进行存储,是因为:
- 如果直接按照
requestInterceptorChain
和responseInterceptorChain
的存储方法,数组的长度将是当前这种方法的四倍。 - 移除、遍历拦截器将会比较麻烦,因为除了需要计算偏移量外,还比现在多做了三步重复操作。
而 requestInterceptorChain
和 responseInterceptorChain
之所以设计成那样的存储方法,其实主要是为了贴合 chain
这个概念。
eject
/**
* Remove an interceptor from the stack
*
* @param {Number} id The ID that was returned by `use`
*/
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
移除这个代码也是比较简单。不过有一点需要注意,this.handlers[id] = null
,这里之所以用的 null
, 而非 undefined
或 {}
,或者直接移除掉 ,主要是为了后续对拦截器进行遍历时进行已移除拦截器处理。
如果每次移除拦截器,都直接使用 splice
,先不说这种做法会导致一个较高的时间复杂度,如果直接这样做了,移除的是最尾部的还好,如果移除的是头部的或中间的拦截器,就会导致从移除的这个拦截器开始后面所有的拦截器 id
都变更掉了( 看上面就知道这个拦截器 id
其实就是数组索引 ),那再移除拦截器就可能移除错误。
如果直接使用 {}
来移除拦截器,也会提高后续的代码复杂度。因为要么需要判断各个元素是否存在,再决定是否传给 forEach 的回调函数。( 脱 ** 放 *) 或者是直接都传给回调函数,由回调函数进行判断。( 这个更不合理了!啊喂!为什么要把明明拦截器自己可以内部处理掉的事情,给回调函数?拦截器都要做外包了吗?啊喂!而且这样也不符合语义啊喂!明明都移除了,为什么还是存在并且传给回调函数让回调函数自己判断是不是存在啊喂!)
那这么看来,使用 undefined
或者 null
是一个比较好的选择。
之所以不使用 undefined
,很大程序上也是一个语义问题。毕竟 null
代表的是空对象,handlers
数组里面存储的也都是对象类型,而使用 undefined
,虽然判断上相同,但是语义上还是有一定的区别的。
关于 undefined
和 null
的区别参考 Axios 源码分析的第二章,总结就是他俩使用上相差不大,差别就是语义和类型是不一样的。
forEach
/**
* Iterate over all the registered interceptors
*
* This method is particularly useful for skipping over any
* interceptors that may have become `null` calling `eject`.
*
* @param {Function} fn The function to call for each interceptor
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};
这个是拦截器内部自己实现的遍历函数。加上这个,我们已经接触了两个 axios
官方写的 forEach
函数。上一个就是这里面的 utils.forEach
。
这个主要的作用其实就是将拦截器遍历扔给回调函数,可能看起来有些难懂。我们拆分一下:
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
...
});
};
fn
是传入的一个回调函数,我们暂且不管,通过我们之前对 utils.forEach
的了解,utils.forEach
其实和原生的 Array.forEach()
一样,只不过可以对 object
进行遍历,所以 function forEachHandler(h) {}
接受到的 h
其实就是拦截器里面的一个个元素。
简单来说,你可以将 utils.forEach(this.handlers, function forEachHandler(h) {})
换算成 this.handlers.forEach(function forEachHandler(h) {})
。
if (h !== null) {
fn(h);
}
之后我们在看里面的处理逻辑,其实就是判断拦截器是否有效,将没有被移除的拦截器传入回调函数 fn()
中。