当前位置 : 首页 > 购买指南

Axios拦截器原理与执行顺序解析

<|begin▁of▁sentence|># 1. 前言 在之前的文章中,我们介绍了`axios`的拦截器,分别介绍了请求拦截器与响应拦截器,以及它们是如何注册以及调用的。并且还介绍了拦截器的执行顺序,其中有一个比较重要的点就是拦截器的执行顺序是:**先注册的请求拦截器后执行,先注册的响应拦截器先执行**。如下图所示: ![](~@/axios/05/01.png) 那么,拦截器内部是如何实现以上执行顺序的呢?接下来,我们就从源码的角度来研究一下拦截器的内部实现原理。 # 2. 拦截器使用示例 我们先来看一下拦截器的使用示例: ```javascript // 添加请求拦截器 axios.interceptors.request.use( function (config) { // 在发送请求之前做些什么 return config; }, function (error) { // 对请求错误做些什么 return Promise.reject(error); } ); // 添加响应拦截器 axios.interceptors.response.use( function (response) { // 对响应数据做点什么 return response; }, function (error) { // 对响应错误做点什么 return Promise.reject(error); } ); ``` 从上面的示例中我们可以看到: - `axios` 对象上有 `interceptors` 属性,该属性又有 `request` 和 `response` 2 个属性,它们都有一个 `use` 方法; - `use` 方法支持 2 个参数,第一个参数类似 `Promise` 的 `resolve` 函数,第二个参数类似 `Promise` 的 `reject` 函数; - 我们可以在 `resolve` 函数和 `reject` 函数中执行同步代码或者是异步代码逻辑; 并且我们注意到,对于 `resolve` 函数的参数: - 请求拦截器 `resolve` 函数参数是 `config` 对象,这个 `config` 对象就是接下来要发送请求的配置对象; - 响应拦截器 `resolve` 函数参数是 `response` 对象,这个 `response` 对象是请求回来的响应对象; 对于 `reject` 函数的参数则是一个 `error` 对象。 # 3. 拦截器源码分析 ## 3.1 拦截器管理类 通过使用示例我们已经知道:`axios` 对象上有 `interceptors` 属性,该属性又有 `request` 和 `response` 2 个属性,并且它们都有一个 `use` 方法。那么,这个 `interceptors` 属性是从哪里来的呢?通过之前对 `axios` 源码的分析,我们知道 `axios` 对象是通过 `createInstance` 函数创建出来的,在该函数内部有这样一段代码: ```javascript function createInstance(defaultConfig) { var context = new Axios(defaultConfig); var instance = bind(Axios.prototype.request, context); // Copy axios.prototype to instance utils.extend(instance, Axios.prototype, context); // Copy context to instance utils.extend(instance, context); return instance; } ``` 在 `createInstance` 函数内部,首先创建了 `Axios` 实例 `context`,接着创建了 `instance` 函数,该函数是 `Axios.prototype.request` 函数,并且函数的 `this` 指针指向 `context`;然后通过 `extend` 函数将 `Axios.prototype` 上的函数扩展到 `instance` 上,并且函数的 `this` 指针指向 `context`;最后再通过 `extend` 函数将 `context` 上的自身属性扩展到 `instance` 上。 所以,`instance` 上就有了 `context` 上的属性,而在 `Axios` 构造函数中,就有这么一段代码: ```javascript function Axios(instanceConfig) { this.defaults = instanceConfig; this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() }; } ``` 可以看到,在 `Axios` 构造函数内部,在实例上添加了 `interceptors` 属性,该属性又有 `request` 和 `response` 2 个属性,并且它们都是 `InterceptorManager` 类的实例。 所以,接下来我们就来看一下 `InterceptorManager` 拦截器管理类。 ## 3.2 InterceptorManager 构造函数 `InterceptorManager` 拦截器管理类的源码位于 `axios/lib/core/InterceptorManager.js`中,代码如下: ```javascript function InterceptorManager() { this.handlers = []; } ``` 可以看到,`InterceptorManager` 构造函数非常简单,在其内部初始化了一个 `handlers` 空数组,用来存储拦截器。 ## 3.3 use 方法 `InterceptorManager` 的原型上有一个 `use` 方法,该方法就是我们使用拦截器的时候调用的方法,代码如下: ```javascript InterceptorManager.prototype.use = function use(fulfilled, rejected) { this.handlers.push({ fulfilled: fulfilled, rejected: rejected }); return this.handlers.length - 1; }; ``` `use` 方法接收两个参数,分别是 `resolve` 函数和 `reject` 函数;然后向 `handlers` 数组中添加一个对象,该对象有 `fulfilled` 和 `rejected` 两个属性,分别对应传入的 `resolve` 函数和 `reject` 函数;最后返回当前拦截器在 `handlers` 数组中的索引,以便后面可以通过这个索引来移除拦截器。 ## 3.4 eject 方法 `InterceptorManager` 的原型上还有一个 `eject` 方法,该方法用来移除拦截器,代码如下: ```javascript InterceptorManager.prototype.eject = function eject(id) { if (this.handlers[id]) { this.handlers[id] = null; } }; ``` `eject` 方法接收一个参数 `id`,该参数是拦截器在 `handlers` 数组中的索引,然后通过这个索引将 `handlers` 数组中对应的拦截器设置为 `null`。这里没有使用 `splice` 方法将拦截器从 `handlers` 中删除,而是将其设置为 `null`,是因为如果我们删除了该拦截器,那么 `handlers` 数组的长度就会改变,那么之前记录的索引就会发生变化,所以这里只是将其设置为 `null`,在遍历的时候跳过即可。 ## 3.5 forEach 方法 `InterceptorManager` 的原型上还有一个 `forEach` 方法,该方法用来遍历 `handlers` 数组中的拦截器,代码如下: ```javascript InterceptorManager.prototype.forEach = function forEach(fn) { utils.forEach(this.handlers, function forEachHandler(h) { if (h !== null) { fn(h); } }); }; ``` `forEach` 方法接收一个参数 `fn`,该参数是一个函数,然后在 `handlers` 数组中遍历每一个拦截器,如果拦截器不为 `null`,就调用 `fn` 函数,并将拦截器作为参数传入。 OK,以上就是 `InterceptorManager` 拦截器管理类的所有代码,非常简单,就是用来管理拦截器的,包括添加拦截器、移除拦截器、遍历拦截器。 # 4. 拦截器执行顺序 在之前的文章中,我们介绍过拦截器的执行顺序是:**先注册的请求拦截器后执行,先注册的响应拦截器先执行**。那么,这个顺序是如何实现的呢?接下来,我们就从源码的角度来分析一下。 我们知道,`axios` 的核心是 `Axios.prototype.request` 函数,而拦截器的执行就是在该函数中实现的。所以,我们来看一下 `Axios.prototype.request` 函数的源码,该函数位于 `axios/lib/core/Axios.js` 中。 由于该函数代码较长,我们只截取与拦截器相关的部分: ```javascript Axios.prototype.request = function request(config) { // ... var chain = [dispatchRequest, undefined]; var promise = Promise.resolve(config); this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { chain.unshift(interceptor.fulfilled, interceptor.rejected); }); this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { chain.push(interceptor.fulfilled, interceptor.rejected); }); while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } return promise; }; ``` 首先,定义了一个数组 `chain`,该数组初始值为 `[dispatchRequest, undefined]`,其中 `dispatchRequest` 就是发送请求的函数,`undefined` 是为了占位,因为 `then` 方法接收两个参数,分别是 `resolve` 函数和 `reject` 函数,所以这里用 `undefined` 占位。 然后,创建了一个 `promise` 对象,该 `promise` 对象的状态是 `resolved`,值是 `config`。 接着,遍历请求拦截器,将每一个请求拦截器的 `resolve` 函数和 `reject` 函数通过 `unshift` 方法添加到 `chain` 数组的头部。所以,**先注册的请求拦截器会被添加到 `chain` 数组的头部,后注册的请求拦截器会被添加到 `chain` 数组的头部的前面**。 然后,遍历响应拦截器,将每一个响应拦截器的 `resolve` 函数和 `reject` 函数通过 `push` 方法添加到 `chain` 数组的尾部。所以,**先注册的响应拦截器会被添加到 `chain` 数组的尾部,后注册的响应拦截器会被添加到 `chain` 数组的尾部的后面**。 最后,通过 `while` 循环,每次从 `chain` 数组中取出两个元素,第一个是 `resolve` 函数,第二个是 `reject` 函数,然后通过 `promise.then` 方法依次调用。因为 `promise` 初始状态是 `resolved`,所以会先执行请求拦截器的 `resolve` 函数,然后执行 `dispatchRequest` 函数发送请求,最后执行响应拦截器的 `resolve` 函数。 所以,整个拦截器的执行顺序就是: 1. 先注册的请求拦截器后执行,后注册的请求拦截器先执行; 2. 然后执行 `dispatchRequest` 函数发送请求; 3. 最后先注册的响应拦截器先执行,后注册的响应拦截器后执行。 # 5. 总结 本篇文章我们主要分析了 `axios` 拦截器的实现原理。首先,我们介绍了拦截器的使用示例;然后,我们分析了拦截器管理类 `InterceptorManager` 的源码,包括 `use` 方法、`eject` 方法和 `forEach` 方法;最后,我们分析了拦截器的执行顺序,以及是如何实现的。 通过分析,我们知道了拦截器的执行顺序是:**先注册的请求拦截器后执行,先注册的响应拦截器先执行**。这个顺序是通过将请求拦截器添加到 `chain` 数组的头部,将响应拦截器添加到 `chain` 数组的尾部,然后通过 `promise.then` 方法依次调用实现的。

栏目列表