Кратко
СкопированоAbort — это встроенный объект, который позволяет отменять выполнение любых операций. Появился в ES2018 (ES9) для отмены fetch запросов, но позже его применение расширилось на другие операции.
Как понять
СкопированоAbort - это механизм для отмены операций. С его помощью можно:
- отменять fetch запросы
- удалять обработчики событий
- останавливать стримы
- прерывать любые другие операции
Состоит из:
- Метода
abortдля отмены операции, где( [ reason ] ) reason- необязательный параметр.
При вызове метода abort reason будет доступен через signal. В reason можно передать любое значение: строку, число, объект, ошибку и т.д.
- Свойства
signal, возвращает объект, который является экземпляромAbortсо следующими свойствами и методами:Signal
aborted— булево значение, указывающее было ли выполнено прерывание;reason— причина отмены;onabort— обработчик события отмены;throw— выбрасывает ошибку с причиной отмены, если сигнал в состоянии "отменён".If Aborted ( )
При отмене операций чаще всего возникает ошибка типа "AbortError". Она появляется в трёх случаях:
- Не передан
reasonвabort;( ) - При использовании встроенных API (например
fetch), которые сами создают AbortError; - При создании через
newс именем "AbortError".D O M Exception ( )
В остальных случаях тип ошибки будет зависеть от того, что было передано в reason.
Также у Abort есть статические методы:
AbortSignal— создаёт уже отменённый сигнал;. abort ( [ reason ] ) AbortSignal— создаёт сигнал, который будет отменён через указанное время;. timeout ( milliseconds ) AbortSignal— создаёт сигнал, который будет отменён, если хотя бы один из переданных сигналов отменён.. any ( signals )
Статические методы используются в случаях, когда не нужен контроллер для ручной отмены.
Как пишется
Скопировано
// Создаём контроллерconst controller = new AbortController()const API_URL = 'https://jsonplaceholder.typicode.com'// Делаем запрос с сигналомfetch(`${API_URL}/posts/1`, { signal: controller.signal }) .then((response) => response.json()) .catch((error) => { if (error.name === 'AbortError') { console.log('Запрос был отменён') } })// Отменяем запрос через 2 секундыsetTimeout(() => { controller.abort()}, 2000)
// Создаём контроллер
const controller = new AbortController()
const API_URL = 'https://jsonplaceholder.typicode.com'
// Делаем запрос с сигналом
fetch(`${API_URL}/posts/1`, { signal: controller.signal })
.then((response) => response.json())
.catch((error) => {
if (error.name === 'AbortError') {
console.log('Запрос был отменён')
}
})
// Отменяем запрос через 2 секунды
setTimeout(() => {
controller.abort()
}, 2000)
Использование с событиями
Скопировано
const controller = new AbortController()const { signal } = controllerconst handler = () => console.log('Клик!')// Добавляем обработчик с сигналомelement.addEventListener('click', handler, { signal })// Удаляем обработчик через AbortControllercontroller.abort()// Это аналогично удалению через removeEventListener:element.addEventListener('click', handler)element.removeEventListener('click', handler)
const controller = new AbortController()
const { signal } = controller
const handler = () => console.log('Клик!')
// Добавляем обработчик с сигналом
element.addEventListener('click', handler, { signal })
// Удаляем обработчик через AbortController
controller.abort()
// Это аналогично удалению через removeEventListener:
element.addEventListener('click', handler)
element.removeEventListener('click', handler)
Отмена нескольких операций
СкопированоОдин сигнал можно использовать для отмены нескольких операций:
const controller = new AbortController()const { signal } = controllerconst API_URL = 'https://jsonplaceholder.typicode.com'// Запускаем несколько запросовPromise.all([ fetch(`${API_URL}/posts/1`, { signal }), fetch(`${API_URL}/posts/2`, { signal }), fetch(`${API_URL}/posts/3`, { signal }),]).catch((error) => { if (error.name === 'AbortError') { console.log('Все запросы отменены') }})// Отменяем все запросы одной командойcontroller.abort()
const controller = new AbortController()
const { signal } = controller
const API_URL = 'https://jsonplaceholder.typicode.com'
// Запускаем несколько запросов
Promise.all([
fetch(`${API_URL}/posts/1`, { signal }),
fetch(`${API_URL}/posts/2`, { signal }),
fetch(`${API_URL}/posts/3`, { signal }),
]).catch((error) => {
if (error.name === 'AbortError') {
console.log('Все запросы отменены')
}
})
// Отменяем все запросы одной командой
controller.abort()
Передача причины отмены
СкопированоМожно указать причину отмены, передав её в метод abort:
controller.abort({ name: 'AbortError', message: 'Операция устарела' })// В обработчике ошибкиtry { ...} catch (error) { if (error.name === 'AbortError') { console.log(error.message) // "Операция устарела" }}
controller.abort({ name: 'AbortError', message: 'Операция устарела' })
// В обработчике ошибки
try {
...
} catch (error) {
if (error.name === 'AbortError') {
console.log(error.message) // "Операция устарела"
}
}
Если передать в качестве reason строку, а не объект, то и в обработчик ошибок попадёт строка:
controller.abort('Операция устарела')// В обработчике ошибкиtry { ...} catch (error) { if (error === 'Операция устарела') { console.log(error) }}
controller.abort('Операция устарела')
// В обработчике ошибки
try {
...
} catch (error) {
if (error === 'Операция устарела') {
console.log(error)
}
}
Использование onabort
Скопированоonabort - это свойство для быстрого назначения обработчика события отмены:
// Через onabort - быстро и простоsignal.onabort = () => { console.log('Операция отменена') console.log('Причина:', signal.reason)}// Через addEventListener - больше кодаconst handler = () => { console.log('Операция отменена') console.log('Причина:', signal.reason)}signal.addEventListener('abort', handler)
// Через onabort - быстро и просто
signal.onabort = () => {
console.log('Операция отменена')
console.log('Причина:', signal.reason)
}
// Через addEventListener - больше кода
const handler = () => {
console.log('Операция отменена')
console.log('Причина:', signal.reason)
}
signal.addEventListener('abort', handler)
Плюсы:
- Простой способ узнать момент отмены операции;
- Лаконичный синтаксис;
- Не нужно хранить ссылку на функцию-обработчик.
Минусы:
- Можно установить только один обработчик;
- При повторном присвоении предыдущий обработчик теряется;
- Нет прямого способа удалить обработчик (только присвоить null).
Использование throwIfAborted()
СкопированоМетод throw полезен для проверки состояния сигнала - он выбросит ошибку, если сигнал находится в состоянии "отменён":
controller.abort('Операция устарела')try { // Проверяем состояние сигнала signal.throwIfAborted() // Этот код не выполнится, если сигнал отменён await someAsyncOperation()} catch (error) { console.log(error) // "Операция устарела"}
controller.abort('Операция устарела')
try {
// Проверяем состояние сигнала
signal.throwIfAborted()
// Этот код не выполнится, если сигнал отменён
await someAsyncOperation()
} catch (error) {
console.log(error) // "Операция устарела"
}
Это более декларативный способ проверки состояния сигнала по сравнению с проверкой signal.
Использование AbortSignal.any()
СкопированоAbortSignal создаёт сигнал, который будет отменён, если хотя бы один из переданных сигналов отменён:
// Создаем два контроллераconst controller1 = new AbortController()const controller2 = new AbortController()// Создаем сигнал, который сработает при отмене любого из контроллеровconst signal = AbortSignal.any([controller1.signal, controller2.signal])// Используем общий сигнал для запросаfetch(url, { signal }) .then((response) => response.json()) .catch((error) => { if (error.name === 'AbortError') { console.log('Запрос отменён:', error.message) } })// Отмена любого из контроллеров приведёт к отмене запросаcontroller1.abort({ name: 'AbortError', message:'Отмена через первый контроллер'}) controller2.abort({ name: 'AbortError', message:'Отмена через второй контроллер'})
// Создаем два контроллера
const controller1 = new AbortController()
const controller2 = new AbortController()
// Создаем сигнал, который сработает при отмене любого из контроллеров
const signal = AbortSignal.any([controller1.signal, controller2.signal])
// Используем общий сигнал для запроса
fetch(url, { signal })
.then((response) => response.json())
.catch((error) => {
if (error.name === 'AbortError') {
console.log('Запрос отменён:', error.message)
}
})
// Отмена любого из контроллеров приведёт к отмене запроса
controller1.abort({ name: 'AbortError', message:'Отмена через первый контроллер'})
controller2.abort({ name: 'AbortError', message:'Отмена через второй контроллер'})
Это полезно для отмены нескольких операций, которые могут быть отменены независимо друг от друга.
Использование AbortSignal.timeout()
СкопированоAbortSignal создаёт сигнал, который будет автоматически отменён через указанное количество миллисекунд:
// Создаем сигнал с таймаутом в 5 секундconst signal = AbortSignal.timeout(2000)// Используем сигнал для запросаfetch(url, { signal }) .then((response) => response.json()) .catch((error) => { if (error.name === 'TimeoutError') { // При таймауте reason будет установлен как "TimeoutError" DOMException console.log('Запрос отменён по таймауту:', error.message) } })
// Создаем сигнал с таймаутом в 5 секунд
const signal = AbortSignal.timeout(2000)
// Используем сигнал для запроса
fetch(url, { signal })
.then((response) => response.json())
.catch((error) => {
if (error.name === 'TimeoutError') {
// При таймауте reason будет установлен как "TimeoutError" DOMException
console.log('Запрос отменён по таймауту:', error.message)
}
})
Это полезно для отмены долгих операций, которые могут занять больше времени, чем ожидалось. Удобная альтернатива ручной установке таймера с set и созданию Abort.
Подсказки
Скопировано💡 Создавайте новый контроллер для каждой группы связанных операций. После вызова abort сигнал остаётся в состоянии "отменён", поэтому для новых операций нужно создать новый контроллер. Не используйте один контроллер для всего приложения.
💡 Метод abort нужно вызывать только в контексте контроллера: controller. Деструктуризация метода приведёт к потере контекста.
- Chrome 66, поддерживается
- Edge 16, поддерживается
- Firefox 57, поддерживается
- Safari 12.1, поддерживается
На практике
Скопированосоветует
Скопировано🛠 Abort упрощает отмену асинхронных запросов в React-компоненте. Это особенно полезно при использовании React, чтобы избежать лишних запросов к серверу, так как Strict в development режиме запускает дополнительный цикл установки и сброса use.
function SearchComponent() { const [search, setSearch] = useState('') const API_URL = 'https://jsonplaceholder.typicode.com' useEffect(() => { const controller = new AbortController() // Запрос отменится при новом поиске или размонтировании fetch(`${API_URL}/posts?userId=${search}`, { signal: controller.signal }) .then(response => response.json()) .then(data => console.log('Результаты:', data)) .catch(error => { if (error.name === 'AbortError') return console.error(error) }) // Очистка при размонтировании и ререндере return () => controller.abort() }, [search]) return (/* ... */)}
function SearchComponent() {
const [search, setSearch] = useState('')
const API_URL = 'https://jsonplaceholder.typicode.com'
useEffect(() => {
const controller = new AbortController()
// Запрос отменится при новом поиске или размонтировании
fetch(`${API_URL}/posts?userId=${search}`, { signal: controller.signal })
.then(response => response.json())
.then(data => console.log('Результаты:', data))
.catch(error => {
if (error.name === 'AbortError') return
console.error(error)
})
// Очистка при размонтировании и ререндере
return () => controller.abort()
}, [search])
return (/* ... */)
}
Пример отписки от событий:
function EventComponent() { useEffect(() => { const controller = new AbortController() // Один сигнал для всех обработчиков window.addEventListener('resize', onResize, { signal: controller.signal }) window.addEventListener('keydown', onKeyDown, { signal: controller.signal }) // Очистка при размонтировании return () => controller.abort() }, [])}
function EventComponent() {
useEffect(() => {
const controller = new AbortController()
// Один сигнал для всех обработчиков
window.addEventListener('resize', onResize, { signal: controller.signal })
window.addEventListener('keydown', onKeyDown, { signal: controller.signal })
// Очистка при размонтировании
return () => controller.abort()
}, [])
}