Skip to content
On this page

AbortController 可终止任何行为

关于博主:前端程序员,最近专注于 webGis 开发。加微信:MasonChou123,进技术交流群。

前端开发中,取消请求、移除事件监听器以及取消 promise 都是非常常见的需求,以前完成这些事情比较复杂,现在有了 AbortController,可以轻易做到,更加符合直觉。

本文介绍 AbortController 如何做到这些。

AbortController 用法介绍

创建一个 AbortController 实例

JS
const controller = new AbortController()

实例具有两个属性

  • abort,一个方法,调用它,触发 abort 事件,signal 对象的 aborted 属性变成 true;
  • signal,只读属性,一个对象,可添加事件监听。
bash
# signal 对象具有的属性
reason: 'AbortError' # 从 abort 方法传入, 默认 AbortError
aborted: false # 是否已取消,取消后会变成 true

通常需要处理两部分功能:

  1. signal监听 abort 事件,在事件处理器中执行取消操作;
  2. 调用abort方法,触发 abort 事件,参数为取消的原因,无返回值。
js
const controller = new AbortController()
const abortSignal = controller.signal

// 监听取消事件
abortSignal.addEventListener('abort', event => {
  console.log('zqj log 执行取消操作', event)
  console.log(abortSignal)
})

console.log(abortSignal)
// 触发取消事件
controller.abort()

AbortController 如何与需要取消的操作关联呢?

先看如何取消 fetch 、事件监听和 promise。

取消 fetch

js
function sentHttp() {
  const url = 'https://jsonplaceholder.typicode.com/todos'
  const params = {
    title: 'to',
  }
  const queryUrl = `${url}?${new URLSearchParams(params)}`

  const controller = new AbortController()
  const abortSignal = controller.signal
  abortSignal.addEventListener('abort', event => {
    // console.log(event,'zqj log')
    console.log(abortSignal, 'zqj log')
  })
  // 10 毫秒后取消
  setTimeout(() => {
    controller.abort('abort')
  }, 10)

  fetch(queryUrl, {
    signal: abortSignal,
  })
    .then(res => res.json())
    .then(data => {
      console.log(data, 'zqj log')
    })
    .catch(err => {
      console.log(err, 'zqj log')
    })
}
btn.onclick = sentHttp

在一个定时器中执行 controller.abort ,fetch 会 reject,实现了 fetch 的超时功能。

取消事件监听

js
const controller = new AbortController()
eventTarget.addEventListener('event-type', handler, {
  signal: controller.signal,
})
// 取消事件监听
controller.abort()

以拖拽事件为例,看看如何取消事件监听。

拖拽功能,需要先监听鼠标按下事件,在鼠标按下事件处理器中监听鼠标移动和鼠标松开事件,需要在鼠标松开事件处理器中移除鼠标按下和鼠标移动事件。

js
el.addEventListener('mousedown', e => {
  // 希望只有鼠标左键按下时才触发
  if (e.buttons !== 1) return

  const onMousemove = e => {
    if (e.buttons !== 1) return
    /* work */
  }

  const onMouseup = e => {
    if (e.buttons !== 1) return
    // 鼠标松开,移除两个事件监听
    window.removeEventListener('mousemove', onMousemove)
    window.removeEventListener('mouseup', onMouseup)
  }

  window.addEventListener('mousemove', onMousemove)
  window.addEventListener('mouseup', onMouseup) // Can’t use `once: true` here because we want to remove the event only when primary button is up
})

上面使用两个 removeEventListener 移除两个事件监听,使用 AbortController 可一次性移除多个事件监听。

addEventListener 的第三个参数传递 signal 对象,当调用 controller.abort() 时,监听器被移除。

使用 AbortController 移除两个事件监听

js
const el = document.querySelector('div')

el.addEventListener('mousedown', e => {
  console.log(e, 'zqj log')
  // https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent/buttons
  // buttons 为 1 表示鼠标左键按下
  if (e.buttons !== 1) return
  const { offsetX, offsetY } = e

  const controller = new AbortController()

  window.addEventListener(
    'mousemove',
    e => {
      if (e.buttons !== 1) return
      el.style.left = e.pageX - offsetX + 'px'
      el.style.top = e.pageY - offsetY + 'px'
    },
    {
      // 传递信号
      signal: controller.signal,
    }
  )

  window.addEventListener(
    'mouseup',
    e => {
      if (e.buttons !== 1) return
      controller.abort()
    },
    {
      // 传递信号
      signal: controller.signal,
    }
  )
})

上面的例子中,添加鼠标移动和鼠标松开事件时,都传递了相同的 signal 对象,当调用 controller.abort() 时,移除所有监听器。

取消 promise

js
function timeout(duration, signal) {
  return new Promise((resolve, reject) => {
    const handle = setTimeout(resolve, duration)
    signal?.addEventListener('abort', e => {
      clearTimeout(handle)
      reject(new Error('aborted-->'))
    })
  })
}
// Usage
const controller = new AbortController()

timeout(100, controller.signal)
  .then(res => {
    console.log(res, 'zqj log')
  })
  .catch(err => {
    console.log(err, 'zqj log')
    console.log(err.name, 'zqj log')
    console.log(err.message, 'zqj log')
  })
setTimeout(() => {
  controller.abort('abort')
}, 99)

AbortSignal 静态方法

AbortSignal 有两个静态方法,无需创建实例,直接调用。

AbortSignal.timeout(n) 超时终止信号

200 毫秒取消 fetch 请求

js
fetch('https://jsonplaceholder.typicode.com/todos', {
  signal: AbortSignal.timeout(200),
})
  .then(res => res.json())
  .then(data => {
    console.log(data, 'zqj log')
  })
  .catch(err => {
    console.log(err, 'zqj log')
  })

AbortSignal.any([signal1,signal2,...])

signal1,signal2 ... 任意一个信号触发终止信号,使用 AbortSignal.any 的行为都被取消。

fetch 可能需要超时取消,也可能需要手动取消,使用 AbortSignal.any 可以同时取消。

js
const timeoutSignal = AbortSignal.timeout(2000) // 2 秒后超时取消
const manualSignal = new AbortController().signal
fetch('https://jsonplaceholder.typicode.com/todos', {
  signal: AbortSignal.any([timeoutSignal, manualSignal]),
})
  .then(res => res.json())
  .then(data => {
    console.log(data, 'zqj log')
  })
  .catch(err => {
    console.log(err, 'zqj log')
  })
// 1 秒后手动取消
setTimeout(() => {
  manualSignal.abort() // fetch 请求被取消
}, 1000)

使得任何行为都可以取消

经过上面的介绍,AbortController 可以取消 fetch、事件监听和 promise,其实不难发现,它可取消任何行为,只要该行为提供了 signal 接口。

封装一个取消的 setInterval

setInterval 在时间无法做到精确,且取消比较麻烦,使用 AbortController 可以轻松取消。

封装一个取消的 setInterval,解决这两个问题。

js
function setIntervalWithAbort(fn, interval) {
  const controller = new AbortController()
  const signal = controller.signal
  setTimeout(function repeat() {
    if (signal.aborted) return
    fn()
    setTimeout(repeat, interval)
  }, interval)
  return controller
}
// Usage
const controller = setIntervalWithAbort(() => {
  console.log('zqj log')
}, 1000)
// 5 秒后取消
setTimeout(() => {
  controller.abort()
}, 5000)

返回 controller,调用 controller.abort() 取消函数。

setTimeout + 递归解决了 setInterval 的时间不准确问题。

setIntervalWithAbort 被取消时,外部希望收到通知,还可以包装一下 abort 函数,让外部传递函数进来,取消时调用。

js
function setIntervalWithAbort(fn, interval) {
  const controller = new AbortController()
  const signal = controller.signal
  let timer2
  let times = 0
  setTimeout(function repeat() {
    if (signal.aborted) return
    ++times // 记录次数 取消时通知外部
    fn()
    setTimeout(repeat, interval)
  }, interval)
  const abort = callback => {
    controller.abort()
    callback && callback(times)
  }
  return {
    abort,
  }
}
// Usage
const controller2 = setIntervalWithAbort(() => {
  console.log('zqj log')
}, 1000)

setTimeout(() => {
  controller2.abort(times => {
    console.log(times, 'zqj log')
  })
}, 5000)

参考

The AbortController, and Aborting Fetch Requests in Javascript

Using AbortController as an Alternative for Removing Event Listeners

Don't Sleep on AbortController

小结

  • AbortController 是一个非常实用的工具,可以轻松取消请求、移除事件监听器以及取消 promise,更加符合直觉。
  • 两个静态方法 AbortSignal.timeout 和 AbortSignal.any,可以实现超时取消和任意一个信号触发取消。
  • 使用 AbortController 可以终止任何行为,只要该行为提供了 signal 接口。

关于博主:前端程序员,最近专注于 webGis 开发。加微信:MasonChou123,进技术交流群。

Released under the MIT License.