Кратко
Скопировано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('Запрос был отменён') } })// Отменяем запрос через 5 секундsetTimeout(() => { controller.abort()}, 5000)
// Создаём контроллер 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('Запрос был отменён') } }) // Отменяем запрос через 5 секунд setTimeout(() => { controller.abort() }, 5000)
Использование с событиями
Скопировано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('Операция устарела')// В обработчике ошибкиtry { ...} catch (error) { if (error.name === 'AbortError') { console.log(error.message) // "Операция устарела" }}
controller.abort('Операция устарела') // В обработчике ошибки try { ... } catch (error) { if (error.name === 'AbortError') { console.log(error.message) // "Операция устарела" } }
Использование 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.message) // "Операция устарела"}
controller.abort('Операция устарела') try { // Проверяем состояние сигнала signal.throwIfAborted() // Этот код не выполнится, если сигнал отменён await someAsyncOperation() } catch (error) { console.log(error.message) // "Операция устарела" }
Это более декларативный способ проверки состояния сигнала по сравнению с проверкой 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('Отмена через первый контроллер')controller2.abort('Отмена через второй контроллер')
// Создаем два контроллера 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('Отмена через первый контроллер') controller2.abort('Отмена через второй контроллер')
Это полезно для отмены нескольких операций, которые могут быть отменены независимо друг от друга.
Использование AbortSignal.timeout()
СкопированоAbortSignal
создает сигнал, который будет автоматически отменен через указанное количество миллисекунд:
// Создаем сигнал с таймаутом в 5 секундconst signal = AbortSignal.timeout(5000)// Используем сигнал для запросаfetch(url, { signal }) .then((response) => response.json()) .catch((error) => { if (error.name === 'AbortError') { // При таймауте reason будет установлен как "TimeoutError" DOMException console.log('Запрос отменен по таймауту:', error.message) } })
// Создаем сигнал с таймаутом в 5 секунд const signal = AbortSignal.timeout(5000) // Используем сигнал для запроса fetch(url, { signal }) .then((response) => response.json()) .catch((error) => { if (error.name === 'AbortError') { // При таймауте 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() }, []) }