Axios 源码分析(四) Axios 拦截器实现

Axios 源码分析(四) Axios 拦截器实现

Cocytus Elias 87 2023-04-26

本文参加了每周一起学习200行源码共读活动

我们在使用 axios 时都或多或少用过拦截器,主要是针对不同的情况做出不同的处理。比如请求前设置 token ,响应后解析数据等等。

axios 拦截器代码在 lib/core/InterceptorManager.js ,拦截器的实现其实并不复杂。整个拦截器也是用 ES5 的写法实现了一个 类,主函数为 InterceptorManager,话不多说,先看图:

axiosInterceptorManager

从图中我们可以看出,拦截器的的实现很简单,一个 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;
};

这里直接将拦截器 pushhandlers。需要注意的是,这里与上一章中,我们看到的 promise 构建是不一样的,promise 链的原始结构 requestInterceptorChainresponseInterceptorChain 数组里,是 0 及偶数索引是 fulfilled ,奇数索引是 rejected

这里是每一个元素都包含了正常处理函数 fulfilled、异常处理函数 rejected、同步运行属性 synchronous、运行时间判定函数 runWhen

这块之所以是这样进行存储,是因为:

  1. 如果直接按照 requestInterceptorChainresponseInterceptorChain 的存储方法,数组的长度将是当前这种方法的四倍。
  2. 移除、遍历拦截器将会比较麻烦,因为除了需要计算偏移量外,还比现在多做了三步重复操作。

requestInterceptorChainresponseInterceptorChain 之所以设计成那样的存储方法,其实主要是为了贴合 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,虽然判断上相同,但是语义上还是有一定的区别的。

关于 undefinednull 的区别参考 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() 中。