用了这么久的CancelToken,Axios竟推荐用AbortController取而代之 您所在的位置:网站首页 settle函数 用了这么久的CancelToken,Axios竟推荐用AbortController取而代之

用了这么久的CancelToken,Axios竟推荐用AbortController取而代之

2023-10-28 19:48| 来源: 网络整理| 查看: 265

前言

对于取消请求,Axios官方曾经推出了CancelToken来实现该功能。而在 2021 年 10 月推出的AxiosV0.22.0版本中却把CancelToken打上 👎deprecated 的标记,意味废弃。与此同时,推荐 AbortController 来取而代之,如下所示:

image.png

大家也可以点击axios#cancellation来阅读关于图片上的文档出处。

针对这个新的用法,我对此进行学习且总结出这篇文章,这篇文章主要的内容点如下:

AbortController是什么? Axios内部是如何运用AbortController的? 个人分析:Axios为什么推荐用AbortController替代CancelToken?

下面就直接开始进入本文的内容吧。

本文所分析的Axios源码版本为v0.27.2

AbortController是什么?

直接引用MDN AbortController来介绍:

AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。

你可以使用 AbortController.AbortController() 构造函数创建一个新的 AbortController。使用 AbortSignal 对象可以完成与 DOM 请求的通信。

可能上面的概念会有点抽象,下面我直接通过一个例子来展示如何用AbortController中断用XHR请求,代码如下所示:

import { useRef, useState } from "react"; export default function App() { const [message, setMessage] = useState(""); const controller = useRef(); const [loading, setLoading] = useState(false); const requestVideo = () => { setMessage("下载中"); setLoading(true); // 创建AbortController实例且存放到controller上 // 注意这里每次请求都会创建一个新的AbortController实例,是因为AbortController实例调用abort后, // AbortController实例的状态signal就为aborted不能更改 controller.current = new AbortController(); const xhr = new XMLHttpRequest(); xhr.open("get", "https://mdn.github.io/dom-examples/abort-api/sintel.mp4"); xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { setMessage("下载成功"); setLoading(false); } }; // 监听AbortController实例的abort事件,当AbortController实例调用abort方法时,就会触发该事件执行回调 controller.current.signal.addEventListener("abort", () => { setMessage("下载中止"); setLoading(false); xhr.abort(); }); xhr.send(); }; // 调用AbortController实例的abort方法,从而触发上面注册在abort事件的回调的执行 const abortDownload = () => { controller.current.abort(); }; return ( Download video Abort Download {message} ); }

交互效果如下所示:

abortcontroller-xhr.gif

大家也可以在这里CodeSandbox体验上面的代码例子。体验的时候最好把网络环境设为“Slow 3G”,这样子接口响应时间长一点,能及时禁止请求。

image.png

从上面的例子中可知,AbortController的实例abortController只是一个类似观察者模式(如上图所示 👆)中的Subject(即事件派发中心),通过abortController.signal.addEventListener('abort', callback)注册Observer。且负责中断 Web 请求的是这些被注册在Subject上的Observer。且这个Subject是一次性的,即只能notify一次。当abortController.abort被调用时,作为信号状态的abortController.signal的aborted属性(只读值)置为true,表示该信号状态已被取消。

AbortController常用于取消Fetch请求,其取消Fetch请求的代码逻辑非常简洁明了,如下代码所示:

import { useRef, useState } from "react"; export default function App() { const [message, setMessage] = useState(""); const controller = useRef(new AbortController()); const [loading, setLoading] = useState(false); const fetchVideo = () => { setMessage("下载中"); setLoading(true); controller.current = new AbortController(); fetch("https://mdn.github.io/dom-examples/abort-api/sintel.mp4", { // fetch配置中仅需把signal指向AbortController实例的signal即可 signal: controller.current.signal, }) .then(() => { setMessage("下载成功"); setLoading(false); }) .catch((e) => { setMessage("下载错误:" + e.message); setLoading(false); }); }; const abortDownload = () => { controller.current.abort(); }; return ( Download video Abort Download {message} ); }

交互效果和XHR例子的一样,这里就不展示了,想体验的读者可以点击此处Code Sandbox。

对于AbortController的浏览器兼容性如下所示:

image.png

可见,如果项目针对的浏览器版本比较旧,那在Axios上还是乖乖用CancelToken来取消请求比较好。

Axios内部是如何运用AbortController的?

我们直接来看看Axios源码中是如何使用AbortController的。首先要知道,在Axios中负责发出请求的是axios.default.adapter,而在浏览器环境下axios.default.adapter取自'lib/adapters/xhr.js'文件,下面来看看这个文件中涉及到XHR和AbortController和CancelToken(为了方便下面分析CancelToken)的源码:

module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { var onCanceled; // done函数用于在请求结束后注销回调函数,以免发生内存泄漏 function done() { // 如果使用CancelToken实例,则会在下面发出请求逻辑之前通过subscribe注册onCanceled函数 if (config.cancelToken) { config.cancelToken.unsubscribe(onCanceled); } // 如果使用AbortController实例,则会在下面发出请求逻辑之前通过signal.addEventListener监听abort事件且注册onCancel作为回调函数 if (config.signal) { config.signal.removeEventListener("abort", onCanceled); } } var request = new XMLHttpRequest(); var fullPath = buildFullPath(config.baseURL, config.url); request.open( config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true ); function onloadend() { // 生成response对象 var response = { data, status, statusText, headers, config, request }; // settle函数内部根据response.status或config.validateStatus去调用_resolve或_reject settle( function _resolve(value) { resolve(value); done(); }, function _reject(err) { reject(err); done(); }, response ); } if ("onloadend" in request) { // Use onloadend if available request.onloadend = onloadend; } else { // Listen for ready state to emulate onloadend request.onreadystatechange = function handleLoad() { if (!request || request.readyState !== 4) { return; } if ( request.status === 0 && !(request.responseURL && request.responseURL.indexOf("file:") === 0) ) { return; } // onreadystatechange事件会先于onerror或ontimeout事件触发 // 因此onloaded需要在下一个事件循环中执行 setTimeout(onloadend); }; } // Handle browser request cancellation (as opposed to a manual cancellation) request.onabort = function handleAbort() {}; // Handle low level network errors request.onerror = function handleError() {}; // Handle timeout request.ontimeout = function handleTimeout() {}; // 处理用到CancelToken或AbortController的情况 if (config.cancelToken || config.signal) { // 取消请求的函数 onCanceled = function (cancel) { if (!request) { return; } reject( !cancel || (cancel && cancel.type) ? new CanceledError() : cancel ); request.abort(); request = null; }; // 如果是用CancelToken取消请求,则把onCanceled注册到CancelToken实例上, // CancelToken实例本质上是一个观察者模式中的Subject,有关其源码会在下面的章节中分析 config.cancelToken && config.cancelToken.subscribe(onCanceled); // 如果是用AbortController,则先从AbortController实例的signal.aborted判断其是否已调用abort, // 如果已调用,直接执行onCanceled,如果没有则直接在signal上监听其事件,逻辑和开头展示AbortController取消XHR请求的例子一样 // axios.request在调用时,会return一条动态生成的promise链,链上的顺序是: // Promise.resove(config)->所有请求拦截器(onFulfilled,onRejected)->(dispatchRequest,undefined)->所有响应拦截器(onFulfilled,onRejected) // dispatchRequest就是调用config.adapter或default.adapter去发出请求, // 因为存在执行请求拦截器途中,AbortController实例已调用aborted的情况,因此这里要对config.signal.aborted做判断处理 if (config.signal) { config.signal.aborted ? onCanceled() : config.signal.addEventListener("abort", onCanceled); } } request.send(requestData); }); };

关于上面注释中说到的promise链其实涉及到拦截器的分析,想了解更多的可以看我之前写的文章如何避免 axios 拦截器上的代码过多。

根据上面的源码分析可知,AbortContoller的调用方式和上一章节中AbortContoller取消XHR请求的逻辑是一样的,非常浅显易懂。

上面源码中同样也展示了CancelToken实例在其中的运行逻辑:

在请求发出之前,CancelToken实例通过自身方法subscribe注册onCancel函数 在请求结束后,CancelToken实例通过自身方法unsubscribe注销onCancel函数

由此可见,CancelToken实例本质上其实也是一个以观察者模式为原理的事件派发中心。在下面的章节中,我们会顺带学习一下CancelToken的源码。

Axios为什么推荐用AbortController替代CancelToken?

在分析CancelToken被替代之前,我们要先阅读CancelToken源码以学习其内在原理

关于CancelToken的原理

Axios提供了以下方式来运用到CancelToken:

const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios.post( "/user/12345", { name: "new name", }, { cancelToken: source.token, } ); source.cancel();

我们来按照axios.post的执行过程逐步分析CancelToken对应的源码:

首先通过CancelToken.source方法生成source变量:

这里首先要知道生成的source是什么,我们看下关于CancelToken.source的源码:

CancelToken.source = function source() { var cancel; var token = new CancelToken(function executor(c) { cancel = c; }); return { token: token, cancel: cancel, }; };

CancelToken.source返回的是token和cancel都取值于CancelToken实例化过程,那我们直接看CancelToken的构造函数:

function CancelToken(executor) { if (typeof executor !== "function") { throw new TypeError("executor must be a function."); } var resolvePromise; this.promise = new Promise(function promiseExecutor(resolve) { resolvePromise = resolve; }); var token = this; // eslint-disable-next-line func-names this.promise.then(function (cancel) { if (!token._listeners) return; var i; var l = token._listeners.length; for (i = 0; i < l; i++) { token._listeners[i](cancel); } token._listeners = null; }); // source.cancel指向此处的cancel函数 executor(function cancel(message) { // token.reason有值代表cancel已被执行,CancelToken是一个一次性的Subject,notify一次后即失效 if (token.reason) { // Cancellation has already been requested return; } token.reason = new CanceledError(message); // resolvePromise执行时,会执行上面this.promise.then中传入的回调函数。从而把listeners全执行 resolvePromise(token.reason); }); }

在axios.post执行时,调用axios.default.adapter处理发出请求的环节。其中涉及到CancelToken的代码在上一章节分析xhrAdapter源码时已经展示过了,这里就不重复了。下面列出关于CancelToken实例在整个请求过程中的操作:

发出请求前:通过config.cancelToken.subscribe(onCanceled)把onCanceled注册到CancelToken实例里。onCanceled内部含request.abort()中断请求操作。 在请求完成后:通过config.cancelToken.unsubscribe(onCanceled)注销该回调函数。

据此,我们来看看CancelToken中关于subscribe和unsubscribe的源码:

CancelToken.prototype.subscribe = function subscribe(listener) { // 如果CancelToken实例已经执行cancel,直接执行该回调函数 if (this.reason) { listener(this.reason); return; } // 如果CancelToken实例还没执行cancel,则把回调函数放进_listeners里 if (this._listeners) { this._listeners.push(listener); } else { this._listeners = [listener]; } }; // 把回调函数从_listeners中移除 CancelToken.prototype.unsubscribe = function unsubscribe(listener) { if (!this._listeners) { return; } var index = this._listeners.indexOf(listener); if (index !== -1) { this._listeners.splice(index, 1); } };

至此CancelToken的原理分析完,设计逻辑非常简单,其实也是观察者模式的运用。

个人分析

个人分析Axios官方更推荐使用AbortController的原因如下:

保持与fetch一样的调用方式,让开发者更好上手

Axios官方一直保持自身的调用方式与fetch相似,如下所示:

fetch(url,config).then().catch() axios(url,config).then().catch()

而目前fetch唯一中断请求的方式就是与AbortController搭配使用。Axios通过支持与fetch一样调用AbortController实现中断请求的方式,让开发者更方便地从fetch切换到Axios。目前就实用性而言,XHR还是比fetch要好,例如sentry在记录面包屑的接口信息方面,XHR请求可以比fetch请求记录更多的数据。还有目前fetch还不支持onprogress这类上传下载进度事件。

旧版本(v0.22.0之前)的CancelToken存在内存泄露隐患,官方想让更多人升级版本从而减少内存泄露风险

用AbortController来中断请求实在v0.22.0版本支持的。而且在v0.22.0之前,CancelToken的运行过程中出现内存泄露隐患。我们来分析一下为什么存在隐患:

拿v0.21.4版本的源码来分析,当时CancelToken不存在CancelToken.prototype.subscribe和CancelToken.prototype.unsubscribe以及内部属性_listeners,且其构造函数如下所示:

function CancelToken(executor) { if (typeof executor !== 'function') { throw new TypeError('executor must be a function.'); } var resolvePromise; this.promise = new Promise(function promiseExecutor(resolve) { resolvePromise = resolve; }); /** v0.22.0才新增了这段代码,用_listeners记录回调函数 this.promise.then(function (cancel) { if (!token._listeners) return; var i; var l = token._listeners.length; for (i = 0; i < l; i++) { token._listeners[i](cancel); } token._listeners = null; }); */ var token = this; executor(function cancel(message) { if (token.reason) { // Cancellation has already been requested return; } token.reason = new Cancel(message); resolvePromise(token.reason); }); }

在xhrAdapter中只有下面的代码中涉及到CancelToken:

// lib\adapters\xhr.js function xhrAdapter(config) { // .... if (config.cancelToken) { // Handle cancellation config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } request.abort(); reject(cancel); // Clean up request request = null; }); } // .... }

早期的思路是,当CancelToken实例执行cancel方法时,实例内部属性this.promise状态置为fulfilled,从而执行在xhrAdapter中用then传入的onCanceled函数,从而达到取消请求的目的。

在Axios官方教程对CancelToken的描述中,注明了可以给多个请求注入同一个CancelToken,以达到同时取消多个请求的作用,如下所示:

Note: you can cancel several requests with the same cancel token.

const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios.get('/user/1', {cancelToken: source.token}) axios.get('/user/2', {cancelToken: source.token}) // 此操作可同时取消上面两个请求 source.cancel()

这种用法使用场景比较多,例如在对大文件时做切片上传的场景,如果需要实现手动中断上传的功能,可以生成一个CancelToken实例,注入到每一个上传切片的请求上。当用户点击"中断传输"的按钮时,直接执行CancelToken实例的cancel方法即可中断所有请求,代码如下所示:

let cancelToken // 上传函数 function upload(){ // 用于存放切片 const chunks = [] // 每个切片的最大容量为5M const SIZE = 5 * 1024 for(let i = 0; i{ axios('upload',{ method:'post', cancelToken: cancelToken.token }) }) } // 中断上传函数 function cancel(){ // 执行cancel后会中断上面所有切片的上传 cancelToken.cancel() cancelToken = null }

但正是这种玩法存在内存泄露的隐患。假设上面切片上传过程中没有发生中断或者很久才发生中断,则cancelToken.promise会一直存在在内存里,而由于xhrAdapter中cancelToken.promise通过.then(function onCancel(){...})挂载了很多个onCancel。而我们再来看看onCancel源码:

function xhrAdapter(config) { config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } request.abort(); reject(cancel); // Clean up request request = null; }); }

会发现request并不是onCancel的局部变量,那么说request是通过闭包机制访问到的。当一个请求已经结束时,request因为仍被onCancel引用,所以没在gc过程中从内存堆里被清理。而这些request因为每一个都包含了当前上传数据所以占用相当大,所有这些request会一直存在内存堆里,直至cancelToken执行cancel或者cancelToken置为null值时,cancelToken.promise才会被清除。

image.png

如果是在上传单个或者数个非常大的文件,则会非常占用内存从而出现泄露的情况。在axios的issue里就有两个是涉及到这种情况的:#1181,#3001

后来v0.22.0版本中,Axios官方把CancelToken做了大改,改成了上一节中分析到的CancelToken源码的情况。与此同时,v0.22.0也开始支持AbortController。因此官方开始推荐AbortController,想让开发者升级版本到v0.22.0以上的同时,消除CancelToken带来的内存泄露隐患。

减少代码维护量

经历了v0.22.0的大改后,CancelToken的原理和AbortController相似。既然有AbortController这种在功能上完全顶替CancelToken,且浏览器兼容性好的原生API。就没必要在继续维护CancelToken。估计在之后v1.x或者v2.x版本里不再存在CancelToken,也减少代码的维护量。

后记

这篇文章写到这里就结束了,如果觉得有用可以点赞收藏,如果有疑问可以直接在评论留言,欢迎交流 👏👏👏。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有