Недавно во время работы над проектом 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
.