AbortController: Варианты применения для эффективного управления асинхронными операциями
О чем статья?
В современных веб-приложениях асинхронные операции играют ключевую роль. Однако управление ими может быть сложным, особенно когда нужно отменить задачи, уже отправленные на выполнение. До появления AbortController разработчики прибегали к различным костылям, таким как создание глобальных переменных, которые отслеживали состояние запроса или использование оберток над XMLHttpRequest.
AbortController — это класс, представленный в JavaScript, который позволяет управлять асинхронными операциями, такими как Fetch запросы, Promise, fs, setTimeout и setInterval
. С его помощью можно прерывать выполнение асинхронных задач и предотвращать нежелательные побочные эффекты от выполнения задач, которые уже неактуальны. AbortController предоставляет надежный и стандартизированный механизм для управления асинхронными задачами. Он позволяет разработчикам контролировать выполнение асинхронных операций, предотвращать выполнение ненужных запросов и избегать утечек памяти. Кроме того, использование AbortController улучшает производительность и ресурсоемкость веб-приложений. Подробнее об API AbortController и AbortSignal вы может почитать по ссылке.
Перейдем к примерам
Для создания экземпляра AbortController используется конструктор класса:
const controller = new AbortController();
// После создания экземпляра AbortController, можно получить экземпляр AbortSignal, используя свойство signal:
const signal = controller.signal;
// Имитация отмены запроса через 3 секунды
setTimeout(() => {
controller.abort();
}, 3000);
fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch request aborted');
} else {
console.error('Fetch request failed:', error);
}
});
В этом примере через 3 секунды после начала запроса к API будет вызван метод abort()
, что приведет к отмене Fetch запроса. Если запрос не будет завершен в течение 3 секунд, обработчик ошибок перехватит событие отмены и выведет соответствующее сообщение в консоль. Если же запрос успеет завершиться раньше, результат будет обработан и выведен в консоль без отмены.
При использовании AbortController важно правильно обрабатывать возможные ошибки. Когда операция отменяется, она обычно вызывает ошибку AbortError
. Это позволяет определить, была ли операция завершена успешно или отменена или завершилась по другой причине.
catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch request aborted');
} else {
console.error('Fetch request failed:', error);
}
)
Следует помнить что не все браузеры и окружения поддерживают AbortController. Для обеспечения обратной совместимости рекомендуется проверять наличие поддержки AbortController перед его использованием:
if ('AbortController' in window) {
// Используем AbortController
} else {
// Используем альтернативное решение или продолжаем без отмены операций
}
Отмена Promise
function delay(duration, signal) {
return new Promise((resolve, reject) => {
if (signal.aborted) {
return reject(new DOMException('Operation aborted', 'AbortError'));
}
const timeoutId = setTimeout(() => {
resolve();
}, duration);
signal.addEventListener('abort', () => {
clearTimeout(timeoutId);
reject(new DOMException('Operation aborted', 'AbortError'));
});
});
}
const controller = new AbortController();
const signal = controller.signal;
delay(5000, signal)
.then(() => {
console.log('Promise resolved');
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Promise aborted');
} else {
console.error('Promise failed:', error);
}
});
// Отменяем промис через 3 секунды
setTimeout(() => {
controller.abort();
}, 3000);
Отмена setTimeout и setInterval
function createInterval(callback, interval, signal) {
if (signal.aborted) {
return;
}
const intervalId = setInterval(() => {
callback();
if (signal.aborted) {
clearInterval(intervalId);
}
}, interval);
signal.addEventListener('abort', () => {
clearInterval(intervalId);
});
}
const controller = new AbortController();
const signal = controller.signal;
createInterval(() => {
console.log('Interval callback executed');
}, 1000, signal);
// Отменяем интервал через 5 секунд
setTimeout(() => {
controller.abort();
}, 5000);
Управление параллельными и последовательными асинхронными операциями
const urls = [
'https://api.example.com/data1',
'https://api.example.com/data2',
'https://api.example.com/data3'
];
const controller = new AbortController();
const signal = controller.signal;
function fetchWithSignal(url, signal) {
return fetch(url, { signal }).then(response => response.json());
}
Promise.all(urls.map(url => fetchWithSignal(url, signal)))
.then(results => {
console.log('All fetch requests completed:', results);
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('One or more fetch requests aborted');
} else {
console.error('One or more fetch requests failed:', error);
}
});
// Отменяем все запросы через 3 секунды
setTimeout(() => {
controller.abort();
}, 3000);
Создание кастомного AbortController с timeout
В некоторых случаях полезно автоматически отменять асинхронные операции, если они не выполняются в заданный период времени. Для этого можно создать кастомный AbortController с таймаутом:
class TimeoutAbortController extends AbortController {
constructor(timeout) {
super();
setTimeout(() => {
this.abort();
}, timeout);
}
}
const controller = new TimeoutAbortController(3000);
const signal = controller.signal;
fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch request aborted');
} else {
console.error('Fetch request failed:', error);
}
});
Последний пример =)
С версии Node.js 10.0.0, многие функции модуля fs
поддерживают промисы и могут использовать AbortController. В этом примере мы используем fs.promises.readFile()
с AbortController для отмены чтения файла:
const fs = require('fs').promises;
const { AbortController } = require('abort-controller');
const controller = new AbortController();
const signal = controller.signal;
async function readWithAbort(path, signal) {
try {
const data = await fs.readFile(path, { encoding: 'utf-8', signal });
console.log('File contents:', data);
} catch (error) {
if (error.name === 'AbortError') {
console.log('File read operation aborted');
} else {
console.error('File read operation failed:', error);
}
}
}
const filePath = './example.txt';
// Отменяем чтение файла через 3 секунды
setTimeout(() => {
controller.abort();
}, 3000);
readWithAbort(filePath, signal);
Вместо заключения
Используйте AbortController только тогда, когда действительно нужно отменять асинхронные операции. В некоторых случаях альтернативные подходы могут быть более подходящими (например, игнорирование результата, если он неактуален).
Обрабатывайте ошибки, связанные с отменой операций, чтобы предоставить пользователю информацию о том, что произошло и каковы возможные следующие шаги.
Очищайте ресурсы после отмены операции. Например, при использовании
setTimeout
илиsetInterval
, не забудьте вызватьclearTimeout
илиclearInterval
при отмене.В случае отсутствия поддержки AbortController, предоставляйте альтернативные решения или информируйте пользователя о возможных ограничениях.
Соблюдая эти лучшие правила, вы сможете максимально эффективно использовать AbortController для управления асинхронными операциями и обеспечения стабильной работы вашего приложения.