О чем статья?

В современных веб-приложениях асинхронные операции играют ключевую роль. Однако управление ими может быть сложным, особенно когда нужно отменить задачи, уже отправленные на выполнение. До появления 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);

Вместо заключения

  1. Используйте AbortController только тогда, когда действительно нужно отменять асинхронные операции. В некоторых случаях альтернативные подходы могут быть более подходящими (например, игнорирование результата, если он неактуален).

  2. Обрабатывайте ошибки, связанные с отменой операций, чтобы предоставить пользователю информацию о том, что произошло и каковы возможные следующие шаги.

  3. Очищайте ресурсы после отмены операции. Например, при использовании setTimeout или setInterval, не забудьте вызвать clearTimeout или clearInterval при отмене.

  4. В случае отсутствия поддержки AbortController, предоставляйте альтернативные решения или информируйте пользователя о возможных ограничениях.

Соблюдая эти лучшие правила, вы сможете максимально эффективно использовать AbortController для управления асинхронными операциями и обеспечения стабильной работы вашего приложения.