Реализация отменяемой асинхронной задержки в JavaScript

Недавно во время работы над проектом React у меня возникла необходимость периодически обновлять некоторое состояние данными из API, полученными с помощью fetch(). Будучи специалистом по C#, я бы решил эту проблему следующим образом:

private async Task FetchDataContinuouslyAsync(CancellationToken cancellationToken)
{
    while (!cancellationToken.IsCancellationRequested)
    {
        await FetchDataAndSetStateAsync(cancellationToken);

        // now wait for 15 seconds before trying again
        await Task.Delay(15000, cancellationToken);
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Естественно, я попытался решить эту проблему таким же образом в JavaScript. Но тут я столкнулся с проблемой — в JavaScript нет встроенной функции, аналогичной Task.Delay().

Это означало, что я должен был придумать собственное решение проблемы. Поиск в интернете дал много результатов, где люди использовали setTimeout вместе с Promise, но удивительно мало тех, которые поддерживали раннюю отмену — а те, что поддерживали, возвращали функцию cancel, а не маркер для отмены. Поскольку я уже использовал fetch() с AbortController для отмены запросов, я хотел повторно использовать этот контроллер для отмены.

Вот что я придумал:

/**
 * Return a promise that is resolved after a given delay, or after being cancelled.
 * 
 * @param  {number} duration The delay, in milliseconds.
 * @param  {AbortSignal|null} signal An optional AbortSignal to cancel the delay.
 * 
 * @return {Promise<void>} A promise that is either resolved after the delay, or rejected after the signal is cancelled.
 */
function asyncSleep(duration, signal) {
    function isAbortSignal(val) {
        return typeof val === 'object' && val.constructor.name === AbortSignal.name;
    }

    return new Promise(function (resolve, reject) {
        let timeoutHandle = null;

        function handleAbortEvent() {        
            if (timeoutHandle !== null) {
                clearTimeout(timeoutHandle);
            }

            if (signal !== null && isAbortSignal(signal)) {
                signal.removeEventListener('abort', handleAbortEvent);
            }

            reject(new DOMException('Sleep aborted', 'AbortError'));
        }

        if (signal !== null && isAbortSignal(signal)) {
            signal.addEventListener('abort', handleAbortEvent);
        }

        timeoutHandle = setTimeout(function () {        
            if (signal !== null && isAbortSignal(signal)) {
                signal.removeEventListener('abort', handleAbortEvent);
            }

            resolve();
        }, duration);
    });
}
Вход в полноэкранный режим Выход из полноэкранного режима

Эта функция принимает задержку в миллисекундах в качестве первого параметра и необязательный AbortSignal в качестве второго параметра. Она возвращает Promise<void>, который разрешится после указанной задержки, или будет отклонен с AbortError, если запрашивается отмена.

В контексте проекта React это можно использовать следующим образом в рамках хука useEffect:

useEffect(() => {
    const ac = new AbortController();

    async function fetchDataContinuously(abortController) {
        while (!abortController.signal.aborted) {
            try {
                await getData(abortController.signal);

                await asyncSleep(refreshInterval, abortController.signal);
            } catch (e) {
                if (e.name === 'AbortError') {
                    break;
                }

                console.error('Error continuously refreshing', e);
            }
        }
    }

    fetchDataContinuously(ac).catch(console.error);

    return () => {
        ac.abort();
    };
}, []);
Войти в полноэкранный режим Выход из полноэкранного режима

Конечно, это можно использовать и с традиционным React компонентом на основе классов, просто прервав AbortController в componentWillUnmount.

Оцените статью
devanswers.ru
Добавить комментарий