Aborting a signal:如何中断一个异步请求?

1,487 阅读5分钟

想象这样一个场景:在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
  1. 首先创建一个AbortController DOM接口的实例;
  2. 将该实例的signal属性绑定到一个变量上;
  3. 执行fetch()方法,并将signal作为其options;
  4. 调用abortController.abort()方法来中断fetch()的执行;
  5. 此时,控制流会传递到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>

下面我们分析下以上代码:

  1. 所有代码都包含在了一个代码块内,这相当于一个IIFE立即执行函数;
  2. 定义abortController变量并初始化为null,由于在代码块内,这样,abortController就不会泄露到全局作用域;
  3. 点击按钮,新建一个AbortController实例,并赋值给abortController变量;
  4. 将AbortController实例的signal属性传递给calculate()函数;
  5. 如果在5秒内再次点击按钮,就会触发abortController.abort()
  6. 由于在calculate()内,abortSignal监听了abort事件,所以会触发该事件;
  7. 在abort事件处理函数内,移除定时器,这时异步任务就中断了;
  8. 抛出DOMException异常(根据规范,AbortError类型必须是DOMException)。

最后总结一下

  • 使用中断异步任务功能,可以减少不必要的请求,提升页面的性能。
  • 有了可中断函数,我们就有后悔药可吃啦!O(∩_∩)O哈哈~

怎么样?很有意思吧!赶紧在你的项目玩起来!

文章最新发表于我的微信公众号,欢迎关注,持续更新中!

前端酱的日常微信公众号二维码
前端酱的日常微信公众号二维码

本文使用 mdnice 排版