Axios的新欢,AbortController是如何取消请求的

前言

按官网的说法,从 v0.22.0 开始,不建议在新项目里使用 CancelToken 的方式来取消请求,而是使用 AbortController,下面就让我们看看两者的运行原理,以及其他相关问题在源码层面的解析(源码基于1.6.8版本)。

CancelToken 和 AbortController

1. CancelToken

为了更好的理通思路,这里需要引入 3段代码 + 1张关系图

代码1(官网例子)

js 复制代码
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

代码2 node_modules/axios/lib/cancel/cancelToken.js

js 复制代码
subscribe(listener) {
    if (this.reason) {
      listener(this.reason);
      return;
    }
    if (this._listeners) {
      this._listeners.push(listener);
    } else {
      this._listeners = [listener];
    }
 }
 let resolvePromise;
 const token = this;
 this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
 });
 executor(function cancel(message, config, request) {
      // 判断是否已经取消过了
      if (token.reason) {
        // Cancellation has already been requested
        return;
      }
      // 给token.reason赋值一个异常
      token.reason = new CanceledError(message, config, request);
      // 传递异常,并将Promise状态改为fulfilled
      resolvePromise(token.reason);
 });
    
 * Returns an object that contains a new `CancelToken` and a function that, when called,
 * cancels the `CancelToken`.
 */
 static source() {
    let cancel;
    const token = new CancelToken(function executor(c) {
        cancel = c;
    });
    return {
      token,
      cancel
    };
 }

代码3 node_modules/axios/lib/adapters/xhr.js

js 复制代码
let request = new XMLHttpRequest();
if (config.cancelToken || config.signal) {
      // Handle cancellation
      // eslint-disable-next-line func-names
      onCanceled = cancel => {
        if (!request) {
          return;
        }
        reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
        request.abort();
        request = null;
      };

      config.cancelToken && config.cancelToken.subscribe(onCanceled);
      if (config.signal) {
        config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
      }
}

关系图:

逻辑整理:

  1. 代码3中,if (config.cancelToken || config.signal) 说明如果配置了取消请求的参数(目前对两种方式都支持),则会在cancelToken上通过subscribe注册一个 onCanceled(cancel) 事件,并存到_listeners中,而 onCanceled 中的 request.abort 就是用来取消请求的。

  2. 从关系图我们可以发现,官方例子里source.cancel其实就等于右侧圈起来的函数,而这个函数一旦执行,resolvePromise就会变成fulfilled并且携带一个CanceledError参数,然后触发 this.promise.then 中的 token._listeners[i](cancel)即上述的onCanceled(cancel)cancel就是通过promise链传递过来的CanceledError

2. AbortController

介绍这个取消方式前,让我们先看下MDN上对 AbortController 的解释:

AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。 你可以使用 AbortController.AbortController() 构造函数创建一个新的 AbortController

使用 AbortSignal 对象可以完成与 DOM 请求的通信。

翻译一下,就是可以用来终止请求,通过 new AbortController() 创建一个实例对象,该对象下面有signal属性和abort方法,

  • signal:只读,返回一个AbortSignal对象实例。
  • abort:中止一个尚未完成的 Web请求,该函数还可以传入一个参数,用来自定义终止请求的原因,这个参数最终会被signal.reason(下面会介绍)接收。

MDN引用了fetch作为例子,将signal传入fetchfetch配置有个signal选项),内部会监测这个对象的状态,如果这个对象的状态从未终止的状态变为终止的状态的话,并且 fetch 请求还在进行中的话,fetch 请求就会立即失败。其对应的 Promise 的状态就会变为 Rejected

那么要如何更改signal的状态呢?只要调用abort方法就可以

js 复制代码
let controller;
const url = "video.mp4";

const downloadBtn = document.querySelector(".download");
const abortBtn = document.querySelector(".abort");

downloadBtn.addEventListener("click", fetchVideo);

abortBtn.addEventListener("click", () => {
  if (controller) {
    controller.abort();
    console.log("中止下载");
  }
});

function fetchVideo() {
  controller = new AbortController();
  const signal = controller.signal;
  fetch(url, { signal })
    .then((response) => {
      console.log("下载完成", response);
    })
    .catch((err) => {
      console.error(`下载错误:${err.message}`);
    });
}

AbortSignal实例 又是什么呢?

上面提到signal其实是一个AbortSignal对象实例(下面描述用AbortSignal替代signal),而该实例对象有两个属性和两个方法(还有其他的属性方法,这里暂时只需要这4个):

  • aborted :只读,一个Boolean值,表示与之通信的请求是否被终止(true)或未终止。(false

  • reason :只读,可以是任何的 JavaScript 类型的值,上面提到的new AbortController().abort传过来的 自定义终止请求的原因 就在这里接收。

  • abort :返回一个已经被设置为中止的 AbortSignal 实例(AbortSignal.aborted===true),在这个方法里也可以传 自定义终止请求的原因 ,和new AbortController().abort等效。

  • timeout :返回一个在指定事件后将自动终止的 AbortSignal 实例,即设置一个时间段,当这个时间段过去后会自动触发 AbortSignalabort 方法,从而中止一个操作或请求

回到Axios源码 node_modules/axios/lib/adapters/xhr.js:

结合我们前面学习的AbortController,如果abortedtrue,表示请求被中止,所以调用取消请求的函数onCanceled;否则,将onCanceled绑定到给AbortSignalabort函数,只要调用AbortSignal.abort就会触发。

tips:

1.一旦 AbortSignal 被终止(即调用了abort方法),它就不能再被用于中止其他操作。终止后的 AbortSignal 对象将保持在终止状态,无法再次用于中止其他操作或请求。如果需要的话,可以创建新的 AbortSignal 对象来控制。

2.同一个AbortSignal 对象可以同时传递给多个请求,在需要的情况下可以同时取消多个请求

拓展

  1. AbortController可以配合 addEventListener使用,以前如果需要 removeEventListenr(event, callback), callback必须和addEventListener上绑定的是同一个callback,这样写起来其实比较麻烦,而现在可以改成用AbortSignal来控制。

举个🌰:

js 复制代码
    const controller = new AbortController();
    function callback (e) {
      document.addEventListener('mousemove',  (e) => {
          // ...
      },{
           signal: controller.signal  
      });
   }
    document.addEventListener('mousedown', callback);
    document.addEventListener('mouseup', controller.abort);
  1. 用于普通函数:
js 复制代码
function fetchData(url, signal) {
  return new Promise((resolve, reject) => {
    fetch(url, { signal })
      .then(response => {
        resolve(response);
      })
      .catch(error => {
        if (error.name === 'AbortError') {
          reject(new Error('请求已被中止'));
        } else {
          reject(error);
        }
      });
  });
}

const controller = new AbortController();
const signal = controller.signal;

// 发起数据请求
fetchData('https://api.example.com/data', signal)
  .then(data => {
    console.log('成功获取数据:', data);
  })
  .catch(error => {
    console.error('数据请求失败:', error);
  });

// 0.3秒后中止请求
setTimeout(() => {
  controller.abort();
}, 300);
  1. 个人理解,AbortSignal就像一棵树,各种动物都可以往树上爬(绑定事件),但是有一天如果树倒了(被中止aborted),所有的动物也就都掉了下来(取消进程),而且这棵树不会再立起来(终止后的 AbortSignal 无法再次用于中止其他操作或请求),动物们也要去寻找新的树了(创建新的 AbortSignal 对象)。

axios和instance的区别

instance是通过axios.create创建出来的,但是它和axios一样,能发送任意请求,也有默认的配置和拦截器等,那它们到底有什么区别呢? 沿着axios的引用依赖摸索,我们在node_modules/axios/lib/axios.js找到了相关代码。

js 复制代码
/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 *
 * @returns {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig);
  const instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});

  // Copy context to instance
  utils.extend(instance, context, null, {allOwnKeys: true});

  // Factory for creating new instances
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

// Create the default instance to be exported
const axios = createInstance(defaults);

// Expose Axios class to allow class inheritance
axios.Axios = Axios;

// Expose Cancel & CancelToken
axios.CanceledError = CanceledError;
axios.CancelToken = CancelToken;
axios.isCancel = isCancel;
axios.VERSION = VERSION;
axios.toFormData = toFormData;

// Expose AxiosError class
axios.AxiosError = AxiosError;

// alias for CanceledError for backward compatibility
axios.Cancel = axios.CanceledError;

// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};

其中 createInstance 就是我们苦苦寻找的核心点,而且由const axios = createInstance(defaults)也可得知,aixos是通过它创建的。这个函数的基本功能如下:

  1. 创建 Axios实例:context
  2. 通过bindrequestthis指向context,并生成了一个新函数 instancebind方法其实是基于apply封装起来的,详见源码如下:
js 复制代码
export default function bind(fn, thisArg) {
  return function wrap() {
    return fn.apply(thisArg, arguments);
  };
}
  1. 通过工具包utils,将Axios.prototype和实例对象的方法都绑定到instance函数的身上
  2. 然后给instance绑定create方法(就如注释所说,就是个创建实例的工厂函数),这个create就是axios.create的源码,它其实返回的也是通过createInstance创建的函数。

看到这里,会发现axiosinstance都是由一个共同函数创建的(都基于最原始的Axios),难道真的一模一样!?

其实往下看就会发现,还是有差别的

createInstance函数下面,给axios还添加了CanceledErrorCancelTokentoFormData等方法,这些都是通过axios.create创建出来的instance函数所不具有的。

拦截器的执行顺序

如果同时定义多个拦截器,那么他们的执行顺序又是如何呢,上代码:

js 复制代码
axios.interceptors.request.use(function (config) {
    console.log('request---1')
    return config;
  });
  axios.interceptors.request.use(function (config) {
    console.log('request---2')
    return config;
  });
  axios.interceptors.response.use(function (config) {
    console.log('response---1')
    return config;
  });
  axios.interceptors.response.use(function (config) {
    console.log('response---2')
    return config;
  });
  axios.get('https://www.thecocktaildb.com/api/json/v1/1/search.php?s=margarita')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

然后会发现请求拦截器的顺序和代码顺序是反的,而响应拦截器是正序,老样子,找源码去:

从中我们可以发现,请求和响应拦截器其实用的一个类 InterceptorManager,然后在该类中,通过use方法,将定义的拦截器都存到hanlders里。

关键点来了 ,最右边代码拿拦截器实例的时候(foreachInterceptorManager类里的一个方法,用于从hanlders里遍历数据),requestInterceptorChain是用的shift,而responseInterceptorChain用的push,而两种拦截器添加的时候都是用的push,所以依旧用push的响应拦截器顺序不变,而用shift的请求拦截器则发生了倒序。

tips:两个数组方法.apply相当于ES6的拓展运算符

其他小问题

  • 如何判断为绝对URL :找到了这个判断方法,但是好像并没有被引用。然后发现node_modules/axios/lib/core/Axios.js里的buildFullPath方法可以构建出绝对路径,可能因此弃用了?...
  • 配置的优先级 :用户配置和默认配置的优先度是通过node_modules/axios/lib/core/Axios.js里的mergeConfig函数来处理的。具体来说,用户配置会覆盖默认配置的相同属性,但不会完全替换默认配置。这意味着用户可以只提供他们想要覆盖的配置,而不必提供所有配置。以下是mergeConfig函数的简化版本:
js 复制代码
function mergeConfig(defaultConfig, userConfig) {
  const config = { ...defaultConfig };

  // Merge user config into default config
  for (const key in userConfig) {
    if (userConfig[key] !== undefined) {
      config[key] = userConfig[key];
    }
  }

  return config;
}
  • 各种方法是怎么挂载的:
  • params是如何自动拼接到地址栏的:

Axios运行流程

欢迎小伙伴留言讨论,互相学习!

❤❤❤ 如果对你有帮助,记得点赞收藏哦!❤❤❤

相关推荐
学习HCIA的小白1 分钟前
关于浏览器对于HTML实体编码,urlencode,Unicode解析
前端·html
向明天乄12 分钟前
Vue3 后台管理系统模板
前端·vue.js
香蕉可乐荷包蛋32 分钟前
vue 常见ui库对比(element、ant、antV等)
javascript·vue.js·ui
彩旗工作室1 小时前
Web应用开发指南
前端
孙俊熙2 小时前
react中封装一个预览.doc和.docx文件的组件
前端·react.js·前端框架
wuhen_n2 小时前
CSS元素动画篇:基于当前位置的变换动画(四)
前端·css·html·css3·html5
by————组态2 小时前
基于web组态优化策略研究
大数据·前端·物联网·低代码·数学建模·自动化
朝阳392 小时前
Electron Forge【实战】自定义菜单 -- 顶部菜单 vs 右键快捷菜单
前端·javascript·electron
叶浩成5202 小时前
a-upload组件实现文件的上传——.pdf,.ppt,.pptx,.doc,.docx,.xls,.xlsx,.txt
javascript·pdf·powerpoint
程序员Bears3 小时前
现代前端工具链深度解析:从包管理到构建工具的完整指南
前端·python·visual studio code