Асинхронный JavaScript — Примеры
Пример 1 — Три запроса, ожидаемые один за другим
Загрузчик дашборда ожидает три независимых запроса по очереди и считает успешным каждый ответ. Объясните обе проблемы, затем сделайте так, чтобы ожидания перекрывались, а сбои были видны.
async function loadDashboard(id) {
const user = await loadUser(id); // 300ms
const orders = await loadOrders(id); // 400ms
const alerts = await loadAlerts(id); // 200ms
return { user, orders, alerts };
}
// итого: ~900ms
const response = await fetch("/api/orders/" + id);
const orders = await response.json(); // ответ 500 разбирается как успешный
Решение
async function loadOrders(id) {
const response = await fetch("/api/orders/" + id);
if (!response.ok) throw new Error("HTTP " + response.status);
return response.json();
}
async function loadDashboard(id) {
const [user, orders, alerts] = await Promise.all([
loadUser(id), loadOrders(id), loadAlerts(id),
]);
return { user, orders, alerts };
}
// итого: ~400ms — самая медленная операция, а не сумма
Когда запросы ждут по очереди, страница дожидается суммы трёх длительностей, хотя друг от друга эти запросы не зависят вовсе; Promise.all ждёт только самую долгую операцию и отклоняется, если упала хотя бы одна. Вторая проблема тише: при сетевом сбое fetch отклоняется, однако при 404 или 500 переходит в fulfilled, поэтому JSON разбирают по телу ошибки и получают сообщение, указывающее не туда. Проверка response.ok превращает непонятную ошибку разбора в названный статус.
Пример 2 — Поиск, в котором побеждает самый медленный ответ
Запрос запускается на каждое нажатие клавиши. Результат отрисовывается, когда бы он ни пришёл. Набрав «keyboard», можно в итоге увидеть результаты для «key». Скажите, что здесь соревнуется, и заставьте брошенный запрос остановиться.
async function search(term) {
const response = await fetch("/api/search?q=" + term);
const results = await response.json();
render(results);
}
input.addEventListener("input", () => search(input.value));
// ответ для более раннего, короткого запроса может прийти последним
Решение
let controller = null;
async function search(term) {
if (controller) controller.abort();
controller = new AbortController();
try {
const response = await fetch("/api/search?q=" + term, {
signal: controller.signal,
});
if (!response.ok) throw new Error("HTTP " + response.status);
render(await response.json());
} catch (error) {
if (error.name === "AbortError") return; // это отмена, а не сбой
showError(error);
}
}
input.addEventListener("input", () => search(input.value));
Промис отменить нельзя, а вот запрос под ним — можно. AbortController существует ровно для этого: начало нового поиска прерывает тот, что уже в полёте, поэтому устаревший ответ больше не перезапишет свежий. Отмена тоже приходит как ошибка, поэтому её нужно отличать от настоящего сбоя, иначе сообщение об ошибке показывалось бы на каждое нажатие клавиши.