Github Repo: https://github.com/zzdjk6/simple-abortable-promise
Короткий рассказ
Правда: Promise
нельзя прервать или отменить. Это может быть только fulfilled
или rejected
.
Трюк: но мы можем reject
на Promise
пораньше, если захотим :)
Длинная история
Посмотрим правде в глаза, у всех нас есть потребность прервать или отменить Promise
иногда. Например, в одностраничном приложении может потребоваться много времени для загрузки некоторых данных или выполнения некоторой обработки, в то время как пользователь хочет прервать ее и заняться чем-то другим.
К счастью, у нас есть стандартный способ прервать сетевой запрос через Fetch API. То есть мы можем передать AbortSignal
для получения и прервать его через AbortController
:
// Initialize fetch call const controller =
new AbortController;const promise = fetch(url, { signal: controller.signal })
// Abort controller.abort()
Затем Promise
, возвращенный fetch
, будет отклонен с AbortError
, когда вызов fetch
будет прерван.
Однако этот подход не прост в использовании, потому что:
- Нам нужно сохранить ссылку на
AbortController
где-то еще, чтобы вызывать.abort()
при необходимости. - Каждый
AbortController
можно использовать только один раз, и мы должны создавать новые после прерывания. То есть единственный эффективный вызов.abort()
— первый раз. После того, как мы вызвали.abort()
наAbortController
,AbortSignal
используется и остаетсяaborted
. Если вы назначите прерванныйAbortSignal
новому вызову fetch и попытаетесь вызвать.abort()
на его контроллере, эффекта не будет. - Работает только с
fetch
Как насчет того, чтобы распространить это поведение на всех Promises
? Давайте попробуем.
Во-первых, давайте представим, что если AbortablePromise
существует, как мы можем его использовать?
В моей иллюзии мы можем сохранить ссылку AbortablePromise
и просто вызвать .abort()
для этого объекта ссылки, чтобы прервать его, как в коде ниже:
// Init
const promise = new AbortablePromise((resolve, reject) => {...});
// Abort
promise.abort();
Затем, чтобы сделать его простым и совместимым с поведением Fetch API, AbortablePromise
следует отклонять с помощью anAbortError
при его прерывании.
Основываясь на иллюзии, мы можем быстро придумать следующую реализацию:
Основная часть этой реализации для переноса функции executor
и reject
на Promise
при получении события abort
:
const wrappedExecutor: ExecutorFunction<T> = (resolve, reject) => { abortSignal.addEventListener('abort', () => { reject(new AbortError()); }); executor(resolve, reject); };
Также мы присоединяем новый метод abort
к объекту Promise
:
this.abort = () => { abortController.abort(); };
Расширенная история
Очевидно, что наша история не может закончиться на этом. Чтобы сделать AbortablePromise
полезным в реальных случаях, нам нужно сделать немного больше.
В идеале, в расширенной версии AbortablePromise
, я думаю, необходимы следующие возможности:
- При инициализации
AbortablePromise
я хочу получить доступ кabortSignal
внутри функцииexecutor
, чтобы я мог сделать что-то еще, когда произойдет прерывание. Типичным примером является передача вызововabortSignal
вfetch
внутриexecutor
, чтобы вызовыfetch
также прерывались при прерыванииPromise
. - Когда прерываю
AbortablePromise
, я иногда хочу задать ему индивидуальную причину вместо использования по умолчанию. - Я хочу быстро обернуть обычный
Promise
какAbortablePromise
- Я хочу, чтобы это решение работало в разных браузерах. Судя по всему,
AbortController
иAbortSignal
не поддерживаются в IE и некоторых старых версиях браузеров.
Чтобы достичь этих особенностей, мы придумали версию 2:
Приходите и найдите больше на моем github :)