想象这样一个场景:在JavaScript中发起一个异步请求,耗时很久都没有返回,这时候你想中断这个请求,怎么做呢?
幸运的是,JavaScript提供了一种方便的方式来中断一个异步任务。在这篇文章里,会告诉你如何使用它来创建你自己的可中断方法。
Abort Signal中断信号
在ES2015引入Promise,以及出现了好一些支持异步解决方案的Web API之后,就有了取消异步任务的需求。
最初的尝试着眼于创建通用解决方案,该解决方案本可以成为ECMAScript标准的一部分。但是,讨论很快陷入僵局,无法解决问题。
因此,WHATWG准备了自己的解决方案,并以AbortController的形式将其直接引入到DOM中。这种解决方案的缺点是,Node.js中不提供AbortController,所以在Node环境没有任何优雅或官方的方式来取消异步任务。
正如你在DOM规范中所看到的,AbortController是用非常通用的方式描述的。因此,你可以在任何类型的异步API中使用它——甚至是还不存在的API。目前只有Fetch API正式支持它,但是这并不阻碍你在自己的代码中使用它!
AbortController是如何工作的?
首先,我们分析一下AbortController是怎么工作的:
const abortController = new AbortController() // 1
const abortSignal = abortController.signal // 2
fetch('http://example.com', {
signal: abortSignal // 3
}).catch(({ message }) => { // 5
console.log(message)
})
abortController.abort() // 4
- 首先创建一个AbortController DOM接口的实例;
- 将该实例的
signal属性绑定到一个变量上; - 执行fetch()方法,并将signal作为其options;
- 调用
abortController.abort()方法来中断fetch()的执行; - 此时,控制流会传递到catch()区块中。
AbortController实例的signal属性
实际上,signal属性是AbortSignal DOM接口的实例,它有一个aborted属性(该属性用来表示用户是否已经调用了abortController.abort()方法)。
所以,你可以在signal上绑定一个abort监听函数,当abortController.abort()被调用后,就会触发该监听函数。
换句话说,AbortController只是AbortSignal的一个公开(public)接口而已。
Abortable function可中断函数
假设有一个需要做复杂运算的异步函数(比如,它需要异步处理很大一个数组数据)。为了简化,以下例子模拟复杂运算为等待5秒后返回结果。
function calculate() {
return new Promise((resolve, reject) => {
setTimeout(()=> {
resolve(1)
}, 5000)
})
}
calculate().then((result) => {
console.log(result)
})
但是,有时候用户需要中断这样一个耗时比较长的操作:
- 添加一个可以点击中断的按钮;
- 添加中断异步任务的功能。
<button id="calculate">Calculate</button>
<script type="module">
{ // 1
let abortController = null // 2
document.querySelector('#calculate').addEventListener('click', async ({ target }) => {
if (abortController) {
abortController.abort() // 5
abortController = null
target.innerText = 'Calculate'
return
}
abortController = new AbortController() // 3
target.innerText = 'Stop calculation'
try {
const result = await calculate(abortController.signal) // 4
alert(result)
} catch {
alert('WHY DID YOU DO THAT?!') // 9
} finally { // 10
abortController = null
target.innerText = 'Calculate'
}
})
function calculate(abortSignal) {
return new Promise((resolve, reject) => {
const error = new DOMException( 'Calculation aborted by the user', 'AbortError' )
// 以防在abortSignal传进来以前,abortController.abort()就已经被调用了的情况
if (abortSignal.aborted) {
return reject(error)
}
const timeout = setTimeout(()=> {
resolve(1)
}, 5000)
abortSignal.addEventListener('abort', () => { // 6
clearTimeout(timeout) // 7
reject(error) // 8
})
})
}
}
</script>
下面我们分析下以上代码:
- 所有代码都包含在了一个代码块内,这相当于一个IIFE立即执行函数;
- 定义
abortController变量并初始化为null,由于在代码块内,这样,abortController就不会泄露到全局作用域; - 点击按钮,新建一个
AbortController实例,并赋值给abortController变量; - 将AbortController实例的
signal属性传递给calculate()函数; - 如果在5秒内再次点击按钮,就会触发
abortController.abort(); - 由于在calculate()内,
abortSignal监听了abort事件,所以会触发该事件; - 在abort事件处理函数内,移除定时器,这时异步任务就中断了;
- 抛出
DOMException异常(根据规范,AbortError类型必须是DOMException)。
最后总结一下
- 使用中断异步任务功能,可以减少不必要的请求,提升页面的性能。
- 有了可中断函数,我们就有后悔药可吃啦!O(∩_∩)O哈哈~
怎么样?很有意思吧!赶紧在你的项目玩起来!
文章最新发表于我的微信公众号,欢迎关注,持续更新中!

本文使用 mdnice 排版