AbortController: Варианты применения для эффективного управления асинхронными операциями

8dd6562c7e5a1a72f35fc5450613aeaa.jpg

О чем статья?

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

© Habrahabr.ru