关于前端:如何优雅地中断-Promise来试试-AbortController-吧

58次阅读

共计 10227 个字符,预计需要花费 26 分钟才能阅读完成。

欢送大家来到 前端小课堂 的第五期,明天咱们来聊一聊如何终止正在进行中的 Fetch 以及 Promise。文中会跟大家具体介绍这外面的两个要害知识点 AbortControllerAbortSignal。对入手实际比拟感兴趣的同学还能够看对应的视频版本。

大家在平时的开发过程中预计不会常常碰到须要被动勾销一个 Fetch 申请的需要,所以一部分同学可能对这一块常识不是很理解。没有关系,看完这篇文章你就可能把握对于如何终止一个 Fetch 申请或者一个 Promise 的全副技能了。那咱们赶快开始吧~

这篇文章比我预期要花费的工夫和精力还要多,所以文章比拟长,大家当初没工夫浏览的能够先珍藏起来,当前缓缓看。如果感觉这篇文章不错的话,也能够帮忙点个赞,转发反对一下。

应用 AbortController 终止 Fetch 申请

fetch 之前,咱们申请后端的资源应用的形式是通过 XMLHttpRequest 这个构造函数,创立一个 xhr 对象,而后通过这个 xhr 对象进行申请的发送以及接管。

const xhr = new XMLHttpRequest();
xhr.addEventListener('load', function (e) {console.log(this.responseText);
});
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1');
xhr.send();

这个 xhr 上也存在一个 abort 办法用来进行申请的终止操作。然而须要留神的是,这个 abort 的执行过程是比拟含糊的。咱们不分明 abort 在什么时候能够不进行或终止对应的网络申请,又或者如果在调用 abort 办法和获取到申请的资源之间存在竞争条件的时候会产生什么。咱们能够通过简略的代码来实际一下:

// ... 省略掉下面的代码
setTimeout(() => {xhr.abort();
}, 10);

通过增加一个延时,而后勾销掉对应的申请;在控制台能够看到,有时申请曾经获取到后果了,然而却没有打印出对应的后果;有时申请没有获取到对应的后果,然而查看对应的网络的状态却是胜利的。所以这外面有很多的不确定性,跟咱们的感觉是比拟含糊的。
等到 fetch 进去的时候,大家就在探讨对于如何正确,分明地勾销一个 fetch 申请。最早的探讨能够看这里 Aborting a fetch #27,那曾经是 7 年前(2015 年)的事件了,能够看到过后的探讨还是比拟强烈的。大家感兴趣的话能够看看过后大家都次要关注的是哪些个性。

最终,新的标准 进去了,通过 AbortControllerAbortSignal 咱们能够不便,快捷,分明地终止一个 fetch 申请。要留神的是,这个标准是一个 DOM 层面的标准,不是 JavaScript 语言层面的标准。当初绝大多数的浏览器环境和新版本的 Node.js 环境也都反对这个个性了。对于 AbortController 的兼容性,大家能够参考这里 AbortController#browser_compatibility

上面文章中的代码例子基本上都能够间接复制粘贴到控制台运行的,所以感兴趣的同学浏览到对应的局部能够间接关上浏览器的控制台去运行一下,而后看看对应的后果。加深一下本人对相干知识点的记忆。

终止正在进行中的单个申请

咱们先通过一段代码来给大家展现一下如何实现这个性能

const ac = new AbortController();
const {signal} = ac;

const resourceUrl = 'https://jsonplaceholder.typicode.com/todos/1';
fetch(resourceUrl, { signal})
  .then(response => response.json())
  .then(json => console.log(json))
  .catch(err => {
    // 不同浏览器的返回后果不同
    console.log(err);
  });

// 能够立刻终止申请,或者设置一个定时器
// ac.abort();
setTimeout(() => {ac.abort();
}, 10);

大家感兴趣的话也能够把下面的代码复制粘贴到浏览器的控制台运行一下,下面代码的运行后果如下所示:

能够看到控制台的 Console 的输入是:DOMException: The user aborted a request.
对应的 Network 展现的是一个勾销状态的申请。这阐明咱们方才发送的申请被终止勾销掉了。
可能在一些特定的状况下被动地勾销相干的申请对咱们利用来说是很重要的,这可能缩小咱们用户的流量应用以及咱们利用的内存应用。

AbortController 的深刻分析

接下来咱们来解说一下下面的代码,第一行通过 AbortController 创立了一个 AbortController 类型的实例 ac,这个实例上有一个 abort 办法和一个 AbortSignal 类型的 signal 实例。而后咱们通过 fetch 办法去申请一个资源门路,传递给 fetch 的选项把 acsignal 对象传递进去。fetch 办法如果获取到了资源就会把资源打印到控制台,如果网络产生了问题,就会捕捉异样,而后把异样打印到控制台。最初,通过一个 setTimeout 延时,调用 acabort 办法终止 fetch 申请。

fetchoptions 选项容许咱们传递一个 signal 对象;fetch 的外部会监测这个对象的状态,如果这个对象的状态从未终止的状态变为终止的状态的话,并且 fetch 申请还在进行中的话,fetch 申请就会立刻失败。其对应的 Promise 的状态就会变为 Rejected

如何扭转 signal 的状态呢?咱们能够通过调用 acabort 办法去扭转 signal 的状态。一旦咱们调用了 ac.abort() 那么与之关联的 signal 的状态会立即从起始状态(非终止状态)转变为终止状态。

咱们下面只是简略地应用了 signal 对象,这个对象是 AbortSignal 类的实例,对于 AbortSignal 咱们上面会做深刻的解说,这里临时只须要晓得 signal 能够作为一个信号对象传递给 fetch 办法,能够用来终止 fetch 的持续进行。
另外,在不同的浏览器中打印的后果可能略有不同,这个跟不同浏览器的外部实现有关系。比方在 Firefox 中的后果如下:

Safari 中的后果如下:

当然如果咱们没有终止 fetch 申请的话,控制台的打印将会是:

另外大家如果须要一些模仿的数据接口的话能够试试 JSONPlaceholder,还是很方便使用的。

批量勾销多个 fetch 申请

值得注意的是,咱们的 signal 对象能够同时传递给多个申请,在须要的状况下能够同时勾销多个申请;咱们来看看如何进行这样的操作。代码如下所示:

const ac = new AbortController();
const {signal} = ac;

const resourcePrefix = 'https://jsonplaceholder.typicode.com/todos/';
function todoRequest (id, { signal} = {}) {return fetch(`${resourcePrefix}${id}`, {signal})
    .then(response => response.json())
    .then(json => console.log(json))
    .catch(e => console.log(e));
}

todoRequest(1, { signal});
todoRequest(2, { signal});
todoRequest(3, { signal});

// 同时终止多个申请
ac.abort();

运行代码后能够在控制台看到如下后果:

如果咱们须要同时对多个申请进行终止操作的的话,应用下面这种形式非常简单不便。

如果咱们想自定义终止申请的起因的话,能够间接在 abort 办法里传递咱们想要的起因,这个参数能够是任何 JavaScript 类型的值。传递的终止的起因会被 signal 接管到,而后放在它的 reason 属性中。这个咱们上面会讲到。

AbortController 的相干属性和办法

具体介绍 AbortSignal

AbortSignal 的属性和办法

AbortSignal 接口继承自 EventTarget,所以 EventTarget 对应的属性和办法,AbortSignal 都继承下来了。当然还有一些本人特有的办法和属性,咱们上面会一一解说到的。须要留神的是,AbortSignal 局部属性有兼容性问题,具体的兼容性大家能够参考这里 AbortSignal#browser_compatibility。

静态方法 abort 和 timeout

这两个办法是 AbortSignal 类上的静态方法,用来发明 AbortSignal 实例。其中 abort 用来发明一个曾经被终止的信号对象。咱们来看上面的例子:

// ... 省略 todoRequest 函数的定义
// Safari 临时不反对,Firefox 和 Chrome 反对
// abort 能够传递终止的起因
const abortedAS = AbortSignal.abort();
// 再发送之前信号终止,申请不会被发送
todoRequest(1, { signal: abortedAS});
console.warn(abortedAS);

运行代码,控制台的输入后果如下:

对应的申请甚至都没有发送进来

咱们也能够给 abort 办法传递终止的起因,比方是一个对象:

// ...
const abortedAS = AbortSignal.abort({
  type: 'USER_ABORT_ACTION',
  msg: '用户终止了操作'
});
// ...

那么输入的后果就如下图所示:

signalreason 属性就变成了咱们自定义的值。

同样的,大家看到 timeout 应该很容易想到是发明一个多少 毫秒 后会被终止的 signal 对象。代码如下:

// ... 省略局部代码
const timeoutAS = AbortSignal.timeout(10);
todoRequest(1, { signal: timeoutAS}).then(() => {console.warn(timeoutAS);
});
console.log(timeoutAS);

代码的运行后果如下:

能够看到咱们打印了两次 timeoutAS,第一次是立刻打印的,第二次是等到申请被终止后打印的。能够看到第一打印的时候,timeoutAS 的状态还是没有被终止的状态。当申请被终止后,第二次打印的结果表明 timeoutAS 这个时候曾经被终止了,并且 reason 属性的值表明了这次申请被终止是因为超时的起因。

属性 aborted 和 reason

AbortSignal 实例有两个属性;一个是 aborted 示意以后信号对象的状态是否是终止的状态,false 是起始状态,示意信号没有被终止,true 示意信号对象曾经被终止了。

reason 属性能够是任何的 JavaScript 类型的值,如果咱们在调用 abort 办法的时候没有传递终止信号的起因,那么就会应用默认的起因。默认的起因有两种,一种是通过 abort 办法终止信号对象,并且没有传递终止的起因,那么这个时候 reason 的默认值就是:DOMException: signal is aborted without reason;如果是通过 timeout 办法终止信号对象,那么这个时候的默认起因就是:DOMException: signal timed out。如果咱们被动传递了终止的起因,那么对应的 reason 的值就是咱们传递进去的值。

实例办法 throwIfAborted

这个办法通过名称大家也能猜出来是什么作用,那就是当调用 throwIfAborted 的时候,如果这个时候 signal 对象的状态是终止的,那么就会抛出一个异样,异样的值就是对应 signalreason 值。能够看上面的代码例子:

const signal = AbortSignal.abort();
signal.throwIfAborted();

// try {//   signal.throwIfAborted();
// } catch (e) {//   console.log(e);
// }

运行后在控制台的输入如下:

能够看到间接抛出异样,这个时候咱们能够通过 try ... catch ... 进行捕捉,而后再进行对应的逻辑解决。这个办法也是很有帮忙的,咱们在前面会讲到。当咱们实现一个自定义的能够被动勾销的 Promise 的时候这个办法就很有用。

事件监听 abort

对于 signal 对象来说,它还能够监听 abort 事件,而后咱们就能够在 signal 被终止的时候做一些额定的操作。上面是事件监听的简略例子:

const ac = new AbortController();
const {signal} = ac;

// 增加事件监听
signal.addEventListener('abort', function (e) {console.log('signal is aborted');
  console.warn(e);
});

setTimeout(() => {ac.abort();
}, 100);

运行后在控制台的输入如下:

能够看到在 signal 被终止的时候,咱们之前增加的事件监听函数就开始运行了。其中 e 示意的是接管到的事件对象,而后这个事件对象上的 targetcurrentTarget 示意的就是对应的 signal 对象。

实现一个能够被动勾销的 Promise

当咱们对 AbortController 以及 AbortSignal 比拟相熟的时候,咱们就能够很不便的结构出咱们自定义的能够勾销的 Promise 了。上面就是一个比较简单的版本,大家能够看一下:

/**
 * 自定义的能够被动勾销的 Promise
 */

function myCoolPromise ({signal}) {return new Promise((resolve, reject) => {
    // 如果刚开始 signal 存在并且是终止的状态能够间接抛出异样
    signal?.throwIfAborted();

    // 异步的操作,这里应用 setTimeout 模仿
    setTimeout(() => {Math.random() > 0.5 ? resolve('ok') : reject(new Error('not good'));
    }, 1000);

    // 增加 abort 事件监听,一旦 signal 状态扭转就将 Promise 的状态扭转为 rejected
    signal?.addEventListener('abort', () => reject(signal?.reason));
  });
}

/**
 * 应用自定义可勾销的 Promise
 */

const ac = new AbortController();
const {signal} = ac;

myCoolPromise({signal}).then((res) => console.log(res), err => console.warn(err));
setTimeout(() => {ac.abort();
}, 100); // 能够更改工夫看不同的后果

这次的代码略微多了一点,不过置信大家还是很容易就晓得下面的代码要示意的是什么意思。

首先咱们自定义了 myCoolPromise 这个函数,而后函数接管一个非必传的 signal 对象;而后立刻返回一个新构建的 Promise,这个 Promise 的外部咱们增加了一些额定的解决。首先咱们判断了 signal 是否存在,如果存在就调用它的 throwIfAborted 办法。因为有可能这个时候 signal 的状态曾经是终止的状态了,须要立刻将 Promise 的状态变更为 rejected 状态。

如果此时 signal 的状态还没有扭转,那么咱们能够给这个 signal 增加一个事件监听,一旦 signal 的状态扭转,咱们就须要立刻去扭转 Promise 的状态。

当咱们上面的 setTimeout 的工夫设置为 100 毫秒的时候,下面的 Promise 总是回绝的状态,所以会看到控制台的打印后果如下:

如果咱们把这个工夫批改为 2000 毫秒 的话,那么控制台输入的后果可能是 ok 也可能是一个 not good 的异样捕捉。

有同学看到这里可能会说,如同不须要 signal 也能够实现被动勾销的 Promise,我能够应用一个一般的 EventTarget 联合 CustomEvent 也能够实现相似的成果。当然咱们也能够这样做,然而个别状况下咱们的异步操作是蕴含网络申请的,如果网络申请应用的是 fetch 办法的话,那么就必须应用 AbortSignal 类型的实例 signal 进行信号的传递;因为 fetch 办法外部会依据 signal 的状态来判断到底需不需要终止正在进行的申请。

AbortSignal 的相干属性和办法:

开发中其余场景的应用举例

勾销事件监听的一种便捷办法

个别状况下,如果咱们对文档中的某个 DOM 元素增加了事件监听,那么当这个元素被销毁或者移除的时候,也须要相应的把对应的事件监听函数移除掉,不然很容易呈现内存透露的问题。所以个别状况下咱们会依照上面的形式增加并且移除相干的事件监听函数。

<button class="event"> 事件监听按钮 </button>
<button class="cancel"> 点击后勾销事件监听 </button>
const evtBtn = document.querySelector('.event');
const cancelBtn = document.querySelector('.cancel');

const evtHandler = (e) => {console.log(e);
};
evtBtn.addEventListener('click', evtHandler);
// 点击 cancelBtn 移除 evtBtn 按钮的 click 事件监听
cancelBtn.addEventListener('click', function () {evtBtn.removeEventListener('click', evtHandler);
});

这种形式是最通用的形式,然而这种形式须要咱们保留对应事件监听函数的援用,比方下面的 evtHandler。一旦咱们失落了这个援用,那么前面就没有方法勾销这个事件监听了。

另外,有些利用场景须要你给某个元素增加很多事件处理函数,勾销的时候就须要一个一个去勾销,很不不便。这个时候咱们的 AbortSignal 就能够派上用场了,咱们能够应用 AbortSignal 来同时勾销很多事件的事件监听函数。就像咱们同时勾销很多个 fetch 申请一样。代码如下:

// ... HTML 局部参考下面的内容

const evtBtn = document.querySelector('.event');
const cancelBtn = document.querySelector('.cancel');

const evtHandler = (e) => console.log(e);
const mdHandler = (e) => console.log(e);
const muHandler = (e) => console.log(e);

const ac = new AbortController();
const {signal} = ac;

evtBtn.addEventListener('click', evtHandler, { signal});
evtBtn.addEventListener('mousedown', mdHandler, { signal});
evtBtn.addEventListener('mouseup', muHandler, { signal});

// 点击 cancelBtn 移除 evtBtn 按钮的 click 事件监听
cancelBtn.addEventListener('click', function () {ac.abort();
});

这样的解决形式是不是就很不便,也十分的分明明了。

addEventListener(type, listener, options);

addEventListener 的第三个参数能够是一个 options 对象,这个对象能够让咱们传递一个 signal 对象用来作为事件勾销的信号对象。就像下面咱们应用 signal 对象来勾销 fetch 申请那样。

从下面的兼容性来说,这个属性的兼容性还是能够的;目前只有 Opera AndroidNode.js 临时还不反对,如果想要应用这个新的属性,须要针对这两个平台和运行环境做一下兼容解决就好了。

一种值得借鉴的解决简单业务逻辑的办法

咱们有时开发中会遇到一些比较复杂的解决操作,比方你要先通过好几个接口获取数据,而后组装数据;而后再把这些数据异步地绘制渲染到页面上。如果用户被动勾销了这个操作或者因为超时了,咱们要被动勾销这些操作。对于这种场景,应用 AbortController 配合 AbortSignal 也有不错的成果,上面举一个简略的例子:

// 多个串行或者并行的网络申请
const requestUserData = (signal) => {// TODO};
// 异步的绘制渲染操作 外面蕴含了 Promise 的解决
const drawAndRenderImg = (signal) => {// TODO};
// 获取服务端数据并且进行数据的绘制和渲染
function fetchServerDataAndDrawImg ({signal}) {signal?.throwIfAborted();
  // 多个网络申请
  requestUserData(signal);
  // 组装数据,开始绘制和渲染
  drawAndRenderImg(signal);
  // ... 一些其余的操作
}

const ac = new AbortController();
const {signal} = ac;

try {fetchServerDataAndDrawImg({ signal});
} catch (e) {console.warn(e);
}

// 用户被动勾销或者超时勾销
setTimeout(() => {ac.abort();
}, 2000);

下面是一个简化的例子,用来示意这种简单的操作;咱们能够看到,如果用户被动勾销或者因为超时勾销操作;咱们下面的代码逻辑能够很不便的解决这种状况。也不会因为少解决了一些操作而导致可能产生的内存透露。

一旦咱们想从新开始这个操作,咱们只须要再次调用 fetchServerDataAndDrawImg 并且传递一个新的 signal 对象就能够了。这样解决后,从新开始和勾销的逻辑就十分分明了。如果大家在本人的我的项目中有相似的这种操作,无妨能够试试这种解决办法。

在 Node.js 中的应用

咱们不仅能够在浏览器环境中应用 AbortControllerAbortSignal,还能够在 Node.js 环境中应用这两个性能。对于 Node.js 中的 fs.readFilefs.writeFilehttp.requesthttps.requesttimers 以及新版本反对的 Fetch API 都能够应用 signal 来进行操作的勾销。上面咱们来举一个简略的例子,对于读取文件的操作:

const fs = require('fs');

const ac = new AbortController();
const {signal} = ac;

fs.readFile('data.json', { signal, encoding: 'utf8'}, (err, data) => {if (err) {console.error(err);
    return;
  }
  console.log(data);
});

ac.abort();

运行代码能够看到终端的输入如下:

常常应用 Node.js 进行业务开发的同学能够尝试应用这个新的个性,应该对开发会很有帮忙的。

反馈和倡议

这篇文章到这里就算完结啦,不晓得有多少同学保持读完了这篇文章;心愿读完的同学都可能把握好这篇文章中解说的常识。如果这篇文章帮到了你,或者关上了你的新世界;欢送点赞转发。

如果你对这篇文章有什么倡议和意见,欢送大家在文章上面留言评论,咱们一起讨论一下,一起提高呀。

往期精彩举荐

  • 【前端小课堂 004 – 如何写一个拖垮你页面性能的正则表达式】
  • 【前端小课堂 003 – 少年,不来试试用正则解决数字的千分位吗?】
  • 【前端小课堂 002 – JavaScript 如何序列化含有循环援用的对象】
  • 【前端小课堂 001 – WeakMap 一点也不 weak】

    参考的相干网址

  • MDN – AbortController
  • DOM Living Standard – Interface AbortController
  • How to Cancel Promise with AbortController
  • Using AbortController as an Alternative for Removing Event Listeners
  • The complete guide to AbortController in Node.js
  • AbortController is your friend
  • Fetch: Abort
  • Abortable fetch
  • EventTarget.addEventListener()
  • MDN – fetch()

正文完
 0