Привет, друзья!
Представляю вашем вниманию адаптированный и дополненный перевод этой замечательной статьи.
AbortController и AbortSignal предоставляют возможность применения некоторых интересных паттернов, рассмотрению которых и посвящена данная статья.
Однако давайте начнем с типичного примера использования AbortController
.
Предположим, что у нас имеется такая разметка:
<div id="app">
<label
>Задержка в мс:
<input type="number" value="5000" id="delayInput" />
</label>
<div>
<p id="logBox"></p>
<pre id="dataBox"></pre>
</div>
<div>
<button id="fetchBtn">Отправить запрос</button>
<button id="abortBtn">Прервать запрос</button>
</div>
</div>
Скрипт:
// в чистом `JS` доступ к элементам с идентификаторами
// можно получать напрямую
// `window.fetchBtn === document.getElementById('fetchBtn')`
fetchBtn.onclick = async () => {
// создаем экземпляр контроллера
const controller = new AbortController()
abortBtn.addEventListener(
'click',
() => {
// прерываем запрос
controller.abort()
},
{ once: true }
)
try {
logBox.textContent = 'Start fetching'
const response = await fetch(
// указываем задержку
`https://jsonplaceholder.typicode.com/users/1?_delay=${delayInput.value}`,
// передаем сигнал
{ signal: controller.signal }
)
logBox.textContent = 'End fetching'
const data = await response.json()
dataBox.textContent = JSON.stringify(data, null, 2)
} catch (e) {
// если запрос был прерван
if (e.name === 'AbortError') {
logBox.textContent = 'Request aborted'
} else {
console.error(e)
}
}
}
Запрос выполняется с задержкой, указанной в соответствующем поле. Если во время выполнения запроса возникает событие нажатия кнопки abortBtn
, выполнение запроса прекращается — выбрасывается исключение AbortError
.
Обратите внимание: запрос может прерываться не только в результате пользовательских действий, но и программно: можно запустить параллельное выполнение нескольких запросов с помощью Promise.race и при завершении первого прервать остальные.
Контроллер и сигнал
Еще раз взглянем на сигнатуру AbortController
:
const controller = new AbortController()
const { signal } = controller
signal
— это экземпляр класса AbortSignal
. Для чего нужны 2 разных класса? Дело в том, что они выполняют разные задачи:
- контроллер позволяет владельцу прерывать сигнал с помощью
controller.abort()
; - сигнал не может прерываться напрямую: его можно либо передавать, например, в
fetch
, либо обрабатывать изменение его состояния вручную.
Прерванное состояние проверяется с помощью signal.aborted
или через обработчик события abort
(fetch
реализует это самостоятельно).
Проще говоря, вещи, которые должны быть прерываемыми, не должны иметь возможности прерывать самих себя, поэтому они получают лишь AbortSignal
.
Случаи использования
Прерывание "легаси" объектов
Некоторые старые части DOM API
не поддерживают AbortSignal
. Одной из таких частей является WebSocket, предоставляющий метод close
для закрытия соединения. Реализовать его прерывание можно следующим образом:
function createAbortableSocket(url, signal) {
const w = new WebSocket(url)
if (signal.aborted) {
w.close() // сигнал прерван, закрываем соединение
}
signal.addEventListener('abort', () => w.close(), { once: true })
return w
}
Обратите внимание: если сигнал уже прерван, событие abort
не возникает — в этом случае мы сразу закрываем соединение с помощью метода close
.
Удаление обработчиков событий
Следующий код работать не будет:
window.addEventListener('resize', () => doSomething())
// не надо так делать
window.removeEventListener('resize', () => doSomething())
Две функции обратного вызова — это разные объекты, поэтому удаление несуществующего обработчика просто тихо завершается неудачей.
AbortSignal
позволяет передать обработчику сигнал для его отмены:
const controller = new AbortController()
const { signal } = controller
window.addEventListener('resize', () => doSomething(), { signal })
// позднее
controller.abort()
Обратите внимание: в старых браузерах приведенный пример работать не будет. Полифил.
Паттерн "Конструктор"
В процессе инкапсуляции кода одним из важных вопросов для решения является вопрос о способе управления жизненным циклом объекта. Это имеет принципиальное значение для кода, который имеет четкие начало и конец выполнения, будь то запрос к API
, рендеринг компонента, открытие сокета и др.
Вот как обычно выглядит такой код:
const someObject = new SomeObject()
someObject.start()
// позднее
someObject.stop()
AbortSignal
позволяет сделать его более эргономичным:
const controller = new AbortController()
const { signal } = controller
const someObject = new SomeObject(signal)
// позднее
controller.abort()
Когда это может пригодиться?
- Это делает использование
SomeObject
однократным, его состояние переходит от запуска до остановки только один раз. Когда требуется другойSomeObject
, просто создается новый экземпляр. - Один
AbortSignal
может распределяться между несколькимиSomeObject
для их одновременной остановки после выполнения связанной группы задач. -
SomeObject
может передавать полученный сигнал дальше, например, вfetch
.
export class SomeObject {
constructor(signal) {
this.signal = signal
// начальный запрос
const r = fetch('https://example.com/some-data', { signal })
}
doSomeComplexOperation() {
if (this.signal.aborted) {
// объект не может использоваться после остановки
throw new Error(`объект остановлен`)
}
for (let i = 0; i < 1_000_000; i += 1) {
// выполняем сложные вычисления
}
}
}
Выполнение асинхронных операций в хуках (P)react
Хук useEffect часто используется для выполнения сетевых запросов. Однако проблема состоит в том, что предыдущий эффект может не успеть завершиться до начала следующего:
function SomeComponent({ someProp }) {
useEffect(() => {
fetch(url + someProp).then().catch().finally()
}, [someProp])
return <></>
}
Вместо этого, предыдущий эффект можно прерывать с помощью AbortController
:
function FooComponent({ someProp }) {
useEffect(() => {
const controller = new AbortController()
const { signal } = controller
;(async () => {
const r = await fetch(url + someProp, { signal })
// ...
})()
return () => controller.abort()
}, [someProp])
return <></>
}
Эту логику можно инкапсулировать в кастомном хуке, например, useEffectAsync
.
Обратите внимание: несмотря на то, что ключевое слово await
приостанавливает выполнение асинхронного кода, состояние хука является стабильным. Другими словами, значения, используемые в хуке, остаются неизменяемыми на протяжении всего жизненного цикла хука:
function SomeComponent() {
const [v, setV] = useState(0)
useEffectAsync(async (signal) => {
await new Promise((r) => setTimeout(r, 1000))
// Какое значение будет иметь `v`?
// Значение этой переменной всегда будет равняться `0`,
// даже если в период задержки будет нажата кнопка
}, [])
return <button onClick={() => setV((v) => v + 1)}>Увеличить</button>
}
Вспомогательные функции
AbortSignal
предоставляет несколько вспомогательных функций.
Обратите внимание: не все эти функции могут быть доступны к моменту чтения данной статьи.
AbortSignal.timeout(ms)
Данная функция создает сигнал, который автоматически прерывается через указанный промежуток времени в мс. Этот функционал можно реализовать самостоятельно следующим образом:
function abortTimeout(ms) {
const controller = new AbortController()
const timerId = setTimeout(() => {
controller.abort()
clearTimeout(timerId)
}, ms)
return controller.signal
}
AbortSignal.any(signals)
Данная функция создает сигнал, который прерывается в случае прекращения любого из переданных ему сигналов. Обратите внимание: если не передать ни одного сигнала, производный сигнал никогда не будет прерван. Полифил:
function abortAny(signals) {
const controller = new AbortController()
signals.forEach((signal) => {
if (signal.aborted) {
controller.abort()
} else {
signal.addEventListener('abort', () => controller.abort(), { once: true })
}
})
return controller.signal
}
AbortSignal.throwIfAborted()
Данная функция просто выбрасывает исключение в случае прерывания сигнала:
if (signal.aborted) {
throw new Error(errMsg)
}
// альтернатива
signal.throwIfAborted()
Полифил может выглядеть так:
function throwIfSignalAborted(signal) {
if (signal.aborted) {
throw new Error(errMsg)
}
}
Пожалуй, это все, чем я хотел поделиться с вами в данной статье. Надеюсь, вы, как и я, узнали для себя что-то новое и не зря потратили время.
Благодарю за внимание и happy coding!
serjJS
Отличная статья, спасибо