AbortController в JavaScript

Источник: «Understanding And Using Abort Controllers In JavaScript»
В веб-разработке управление асинхронными задачами имеет решающее значение для создания отзывчивых и эффективных приложений. Асинхронные операции, такие как получение данных с сервера или выполнение трудоёмких вычислений, часто требуют возможности отменить или прервать их до завершения. Здесь на помощь приходит AbortController, о котором и пойдёт речь.

Контроллеры Отмены (AbortController) — свежее дополнение к языку JavaScript, появившееся как часть спецификации DOM (Document Object Model). Они предоставляют средства для отмены асинхронных задач. Хотя в основном они используются с fetch запросами, они также могут работать с другими асинхронными операциями, такими как функции setTimeout или setInterval.

Контроллер Отмены создаётся путём инстанцирования класса AbortController, как показано ниже:

const controller = new AbortController();

Объект controller имеет метод abort(), который может быть вызван для отмены связанной асинхронной задачи. Чтобы связать контроллер с асинхронной операцией, необходимо передать его свойство signal в качестве опции при инициировании асинхронной операции. Например, для запроса fetch реализуем его так, как показано ниже:

const controller = new AbortController();
fetch("https://api.example.com/data", { signal: controller.signal })
.then((response) => {
// Обработка ответа
})
.catch((error) => {
//обработка прерванного запроса
if (error.name === "AbortError") {
console.log("Request was aborted");
} else {
//обработка других ошибок
console.error("Error occurred:", error);
}
});

Чтобы отменить fetch запрос, можно вызвать метод abort() объекта controller:

controller.abort();

К преимуществам использования AbortController относится следующее:

Примеры использования AbortController

В этом разделе рассмотрим реализацию контроллеров отмены на различных асинхронных событиях, таких как AJAX-запросы, а также с помощью встроенных асинхронных функций Javascript, таких, как setTimeout или setInterval.

Использование AbortController с Fetch API

В этой простой демонстрации рассмотрим практический пример использования AbortController для отмены fetch запроса. Предположим, что в веб-приложении есть две кнопки, одна инициирует fetch запрос для загрузки данных, а другая завершает этот первоначальный запрос. Можно объединить эту функциональность, как показано в примере ниже:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="loadDataBtn">Load Data</button>
<button id="abortFetchBtn">Abort Fetch</button>
<script>
const controller = new AbortController();
const loadBtn = document.getElementById("loadDataBtn");
const abortBtn = document.getElementById("abortFetchBtn");
const getData = (abortSignal) => {
fetch("https://api.example.com/data", { signal: abortSignal })
.then((response) => response.json())
.then((data) => {
// Обработка ответа
})
.catch((error) => {
if (error.name === "AbortError") {
console.log("Request was aborted");
} else {
console.error("Error occurred:", error);
}
});
};
const cancelFetchRequest = () => {
controller.abort();
};
loadBtn.addEventListener("click", () => {
getData(controller.signal);
});
abortBtn.addEventListener("click", () => {
cancelFetchRequest();
});
</script>
</body>
</html>

В этом примере клик по кнопке Load Data инициирует fetch запрос. Если пользователь хочет отменить запрос до его завершения, он может сделать это, нажав кнопку Abort Fetch, которая вызывает функцию cancelFetchRequest() и прерывает связанный fetch запрос.

Использование AbortController с setTimeout и setInterval

Для начала рассмотрим пример с использованием функции setTimeout.

// Создание экземпляра AbortController
const controller = new AbortController();
const signal = controller.signal;

const timeoutId = setTimeout(() => {
console.log("Timeout completed");
}, 5000);

// Если сработал сигнал abort, сбрасываем таймаут.
signal.addEventListener("abort", () => {
clearTimeout(timeoutId);
console.log("Timeout aborted");
});

// Устанавливаем таймаут для прерывания операции через 3 секунды
setTimeout(() => {
controller.abort();
}, 3000);

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

Однако в более реалистичных сценариях, разработчики часто сталкиваются с управлением множеством асинхронных событий и функций; в этом случае AbortController обеспечивает стандартизированный и более предсказуемый подход к управлению этими событиями. Этот сценарий проиллюстрирован в данном примере с помощью функции setInterval и будет более подробно рассмотрен далее.

// Создание экземпляра AbortController
const controller = new AbortController();
const signal = controller.signal;
// Массив для хранения ID интервалов
const intervalIds = [];

// Функция создания и запуска интервалов
const startIntervals = () => {
for (let i = 0; i < 5; i++) {
const intervalId = setInterval(() => {
console.log(`Interval ${i + 1} tick`);
}, (i + 1) * 1000); // Продолжительность интервала увеличивается с каждым интервалом
intervalIds.push(intervalId);
}
};

// Запуск интервалов
startIntervals();

// Если сработал сигнал abort, сбрасываем все интервалы.
signal.addEventListener("abort", () => {
intervalIds.forEach((id) => clearInterval(id));
console.log("Intervals aborted");
});

// Прерывание операции через 7 секунд
setTimeout(() => {
controller.abort();
}, 7000);

Варианты использования AbortController

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

События дебаунсинга

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Debouncing Example with AbortController</title>
</head>
<body>
<form>
<input id="inputField" placeholder="Type something..." />
</form>
<script>
const formInput = document.getElementById("inputField");
let abortController = null;

// Функция для выполнения операции дебаунсинга
const debounceOperation = () => {
const controller = new AbortController();
const signal = controller.signal;
// Выполнение асинхронную операцию, например получение данных
fetch("https://api.example.com/data", { signal })
.then((response) => response.json())
.then((data) => {
// Обработка данных
console.log("Debounced operation completed:", data);
})
.catch((error) => {
if (error.name === "AbortError") {
console.log("Debounced operation was aborted");
} else {
console.error(
"Error occurred during debounced operation:",
error
);
}
});
};

// функция для дебаунсинга событий пользовательского ввода
const debounceEvent = () => {
// Если операция дебаунсинга продолжается, прерываем её.
if (abortController) {
abortController.abort();
}

// Создание AbortController для текущей операции
abortController = new AbortController();
const signal = abortController.signal;

// Запуск нового таймаута для операции дебаунсинга
setTimeout(() => {
debounceOperation();
abortController = null; // Сброс AbortController; в данной реализации не требуется, так как функция дебаунсинга привязана к событию key-up
}, 500); // При необходимости отрегулируйте задержку дебаунсинга
};

// Пример: Дебаунсинг события key-up
formInput.addEventListener("keyup", debounceEvent);
</script>
</body>
</html>

Этот фрагмент кода демонстрирует реализацию дебаунсинга события пользовательского ввода с использованием AbortController в JavaScript. Когда пользователь вводит текст в поле ввода (inputField), слушатель события keyup запускает функцию debounceEvent. Внутри этой функции, если есть текущая операция дебаунсинга (отслеживаемая переменной abortController), она прерывает предыдущую операцию. Затем она создаёт новый экземпляр AbortController и связывает его с текущей операцией. После заданной задержки (в данном случае 500 миллисекунд) он выполняет функцию debounceOperation, выполняющую асинхронную операцию (в данном случае получение данных из API). Если операция завершается в пределах задержки, данные обрабатываются; в противном случае, если она была прервана из-за последующего события, в консоль выводится соответствующее сообщение. Такой подход гарантирует, что только последнее событие запускает операцию, эффективно отменяя события пользовательского ввода, чтобы предотвратить ненужные и повторяющиеся вызовы функций.

Долгий опрос и события, отправляемые сервером

В приложениях, где важны обновления в реальном времени, таких как чат-приложения или прямые спортивные результаты, для поддержания постоянного соединения с сервером обычно используются долгие опросы (long-polling) или события, отправляемые сервером (SSE). Однако могут возникнуть ситуации, когда клиенту необходимо преждевременно прервать соединение, например, перейти со страницы или закрыть вкладку браузера. AbortController позволяет изящно завершать такие соединения, прерывая связанные с ними запросы, предотвращая ненужное потребление ресурсов как на стороне клиента, так и на стороне сервера.

Управление пользовательским взаимодействием

В веб-приложениях пользователи часто вызывают множество асинхронных действий, таких как нажатие кнопки, отправка формы или открытие выпадающего списка. Часто пользователи проявляют нетерпение и могут запускать эти асинхронные события многократно и в неожиданном, случайном порядке, который не соответствует ходу работы веб-приложения.

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

Асинхронное управление задачами

Интеграция AbortController с реактивными фреймворками

Реактивное программирование в последние годы приобрело значительную популярность благодаря способности обрабатывать асинхронные потоки данных и события более предсказуемым и управляемым способом. Многие JavaScript-фреймворки, такие, как React.js, Vue.js и Angular.js, поддерживают парадигму реактивного программирования.

Эти популярные фреймворки поддерживают форму рендеринга приложений, называемую SPA, при которой сначала загружается оболочка (пустая страница), а содержимое постепенно и динамически добавляется на страницу с помощью JavaScript и fetch запросов. Такой подход отлично работает до тех пор, пока пользователь не начнёт взаимодействовать с приложением и переходить на различные страницы. Из-за особенностей таких приложений существует риск утечки памяти при выполнении AJAX-запросов, так как запрос, предназначенный для содержимого одной страницы, будет продолжать выполняться в фоновом режиме, даже если просматривается другая страница.

Эта проблема приводит к замедлению работы приложения и является одной из тех областей, где контроллеры отмены действительно являются оптимальным решением. Используя контроллеры отмены для завершения всех ожидающих запросов на конкретной странице в качестве функции очистки, можно устранить такие утечки и получить более плавное и быстрое приложение. Эта реализация продемонстрирована в приведённых ниже фрагментах кода для фреймворков React.js и Vue.js.

React.js демо

В приложениях React управление асинхронными операциями часто включает в себя хуки, такие как useState и useEffect. При выполнении AJAX-запросов, особенно с помощью хука useEffect, AbortController играет неоценимую роль. Можно использовать эти хуки для создания более отзывчивых, управляемых и отменяемых операций, что поможет облегчить очистку страницы.

import React, { useState, useEffect } from 'react';

function MyComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;

fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request was aborted');
} else {
console.error('Error occurred:', error);
}
setLoading(false);
});

return () => {
controller.abort();
};
}, []); // Пустой массив зависимостей гарантирует, что эффект будет запущен только один раз

return (
<div>
{loading ? <p>Loading...</p> : <p>{data}</p>}
</div>
);
}

export default MyComponent

В приведённом выше фрагменте кода fetch запрос выполняется с помощью хука useEffect, как только MyComponent будет примонтирован. Однако перед тем как размонтировать MyComponent, в функции очистки useEffect используется AbortController, чтобы отменить все текущие запросы.

Vue.js демо

В Vue.js асинхронные операции можно обрабатывать с помощью монтируемых хуков жизненного цикла или свойств watch. Аналогичным образом AbortController может быть интегрирован в компоненты Vue для управления fetch запросами и другими асинхронными задачами.

<template>
<div>
<p v-if="loading">Loading...</p>
<p v-else></p>
</div>
</template>

<script>
export default {
data() {
return {
data: null,
loading: true,
controller: null
};
},
mounted() {
this.controller = new AbortController();
const signal = this.controller.signal;

fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(data => {
this.data = data;
this.loading = false;
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request was aborted');
} else {
console.error('Error occurred:', error);
}
this.loading = false;
});
},
beforeDestroy() {
if (this.controller) {
this.controller.abort();
}
}
};
</script>

В приведённом выше фрагменте кода fetch запрос выполняется, когда компонент Vue смонтирован. Однако перед тем, как компонент будет размонтирован, в функции beforeDestroy используется AbortController, отменяющий все ожидающие запросы.

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

Продвинутые техники и рекомендации

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

Прерывание множества запросов

В комплексных веб-приложениях могут возникать ситуации, когда необходимо одновременно прервать несколько асинхронных запросов, например, при переходе пользователя на новую страницу или выполнении пакетного действия. Для решения этой своеобразной задачи начните с ведения коллекции (например, массива) AbortController, каждый из которых связан с отдельным асинхронным запросом. Когда возникнет необходимость прервать несколько запросов, пройдитесь по коллекции и вызовите abort() на каждом контроллере. Такая техника обеспечивает эффективную отмену всех текущих запросов. Реализация кода показана ниже:

// Массив для хранения AbortController
const abortControllers = [];

// функция для создания и запуска нового асинхронного запроса
const startNewRequest = () => {
const controller = new AbortController();
const signal = controller.signal;

fetch("https://api.example.com/data", { signal })
.then((response) => response.json())
.then((data) => {
// Обработка данных
})
.catch((error) => {
// Обработка ошибок
});

// Добавление контроллера в массив
abortControllers.push(controller);
};

// Функция прерывания всех выполняющихся запросов
const abortAllRequests = () => {
abortControllers.forEach((controller) => {
controller.abort();
});
};

// Пример использования:
startNewRequest(); // Запуск первого запроса
startNewRequest(); // Запуск другого запроса

// Где-то в приложении, когда возникнет необходимость прервать все запросы (например, при переходе на новую страницу):
abortAllRequests();

Обработка ошибок и очистка

Изящная обработка ошибок и выполнение задач по очистке при прерывании запросов очень важны для поддержания стабильности приложения и предотвращения утечек памяти. Всегда следует реализовывать надёжные механизмы обработки ошибок в рамках обратных вызовов fetch или асинхронных функций. Обрабатывайте специфические типы ошибок, такие как прерывание, сетевые или серверные ошибки, соответствующим образом. Кроме того, выполняйте все необходимые задачи по очистке, такие как закрытие соединений, освобождение ресурсов или обновление состояния пользовательского интерфейса, для поддержания согласованности и изящного восстановления после прерванных запросов. Эта реализация показана ниже:

// функция, выполняющая асинхронный запрос
const fetchData = () => {
const controller = new AbortController();
const signal = controller.signal;

fetch("https://api.example.com/data", { signal })
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
})
.then((data) => {
// Обработка данных
})
.catch((error) => {
if (error.name === "AbortError") {
console.log("Request was aborted");
} else {
console.error("Error occurred:", error.message);
}
})
.finally(() => {
// Выполнение задач по очистке
// Например, закрытие соединений, освобождение ресурсов или обновление состояния UI.
console.log("Cleanup tasks performed");
});

// Функция прерывания запроса
const abortRequest = () => {
controller.abort();
};

// Пример использования:
// setTimeout(abortRequest, 5000); // Прервать запрос через 5 секунд
};

// Запуск асинхронного запроса
fetchData();

Поддержка броузерами и полифиллы

Хотя большинство современных браузеров, включая Chrome, Firefox, Safari и Edge, поддерживают AbortController, старые версии браузеров могут его не поддерживать, что потенциально может повлиять на кроссбраузерную совместимость. Чтобы устранить этот недостаток, следует использовать функцию обнаружения возможностей, чтобы определить, поддерживается ли AbortController в браузере пользователя. Если они не поддерживаются, воспользуйтесь библиотекой полифилла, например abortcontroller-polyfill, предоставляющей совместимую реализацию интерфейса AbortController. Включите полифилл в свой проект, чтобы обеспечить согласованное поведение в различных браузерных средах, что позволит вам использовать преимущества AbortController, сохраняя широкую совместимость.

Подведение итогов

AbortController — мощный механизм для управления асинхронными задачами в JavaScript, позволяющий разработчикам изящно отменять выполняющиеся операции. В статье рассмотрены основы использования AbortController, включая их создание, использование с fetch-запросами и интеграцию с другими асинхронными функциями, такими как setTimeout и setInterval. Также рассматриваются продвинутые техники и рекомендации, в том числе использование AbortController с реактивными фреймворками, обработки ошибок, очистки и браузерной поддержки.

Справочная информация

Дополнительные материалы

Предыдущая Статья

Как закоммитить многострочные сообщения в git commit

Следующая Статья

Магические методы PHP