Перейти к содержанию

Отмена устаревшего запроса

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

Используем независимый снимок advanced/lesson-22 из архива. В нём остаются четыре курса, локальные страницы JSON и функции чтения URL прошлого урока. Меняется управление попыткой загрузки. Все результаты далее ожидаемые: учебный сервер и программа при подготовке не запускались.

Сигнал и операция

Контроллер создаёт объект signal, который передаётся поддерживающей отмену операции. Вызов abort меняет состояние сигнала и уведомляет участников. Контроллер не является самим запросом и не содержит полученные курсы. Один сигнал может быть связан с несколькими действиями одной попытки.

В нашем mock-api.js сигнал передан в fetch. После разбора JSON учебный источник может дополнительно выдержать искусственную задержку. Она также принимает сигнал и снимает свой таймер при отмене. Это не измерение длительности сети: задержка нужна только для наблюдаемого жизненного цикла в небольшом локальном примере.

const response = await fetch(`./data/pages/${topic}-${page}.json`, {
  signal,
  cache: 'no-store',
});

Фрагмент находится внутри существующей loadPage; переменные topic, page и signal являются её параметрами. Не вставляйте его отдельным вызовом в приложение без окружения. Полная функция читает конверт, проверяет записи и возвращает страницу как прежде.

Механизм контроллера и сигнала объясняется в MDN об AbortController. Отмена способна затронуть загрузку запроса и чтение тела. Она не обещает отмену уже совершённой серверной операции: если запрос изменял данные, сервер мог выполнить действие раньше получения сигнала.

Новая попытка отменяет прежнюю

В app.js хранится контроллер текущей попытки. Следующий блок полностью заменяет функцию обновления из урока 21 и добавляет одно внешнее объявление; остальные импорты, DOM-узлы и обработчики формы уже есть в снимке.

let activeController = null;
async function refresh(next, write = false) {
  activeController?.abort();
  const controller = new AbortController();
  activeController = controller;
  status.textContent = 'Загружаем страницу…';
  try {
    const result = await loadPage(next, {
      signal: controller.signal,
      delayMs: 600,
    });
    if (controller.signal.aborted) return;
    selection = next;
    topicField.value = next.topic;
    renderCourses(list, result.items);
    status.textContent = `Показано: ${result.items.length}. Всего: ${result.total}.`;
    if (write) writeSelection(next);
  } catch (error) {
    if (controller.signal.aborted) return;
    status.textContent = 'Страница не загружена.';
    console.error(error);
  }
}

При входе сначала отменяется предыдущая попытка, затем создаётся новый контроллер. Повторное использование старого отменённого сигнала было бы ошибкой: его состояние не возвращается к первоначальному. Каждой новой загрузке нужен собственный объект, даже если тема совпала.

Локальная переменная controller относится к конкретному вызову. В catch проверяется именно её сигнал, а не значение внешнего activeController, которое уже могло указывать на следующую попытку. Это существенная разница: новый запрос не должен заставить обработчик старого отказа проверять состояние другой операции.

Отмена и сообщение ошибки

Когда операция была отменена потому, что пользователь выбрал другую тему, показывать «Страница не загружена» от старого вызова не нужно. Новая попытка уже обновила статус ожидания. Наш ранний выход сохраняет её сообщение и не очищает результаты по причине устаревшего выбора.

Проверка signal.aborted выражает политику попытки. Проверка только error.name === 'AbortError' часто встречается в небольших примерах, но не является полным договором причин отмены: контроллер допускает собственную причину. В этом снимке контроллер создаётся приложением, поэтому сигнал является надёжной связью отказа с нашей попыткой.

Иные ошибки всё ещё журналируются и показываются читателю. Отсутствующий JSON даёт HTTP-отказ, неверный конверт вызывает исключение модели. Не следует игнорировать любой отказ после добавления отмены: тогда повреждённый источник превратился бы в бесконечное ожидание без объяснения.

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

Задержка, которая умеет завершаться

Учебная функция ожидания создаёт Promise с таймером и обработчиком abort. Когда таймер завершается, обработчик снимается; когда приходит отмена, таймер очищается. Таким образом, нормальный и отменённый исходы освобождают неиспользуемое действие. Если оставить слушатель навсегда, последующие операции сложнее анализировать.

Если сигнал уже отменён до начала задержки, Promise сразу отклоняется с его причиной. Проверка нужна до регистрации, потому что событие отмены уже произошло и не обязано повторяться ради нового слушателя. Это общий принцип работы с состоянием и событиями: текущее состояние сначала читается, затем регистрируется ожидание изменения.

Таймер в снимке работает после чтения JSON. Поэтому нельзя утверждать, что весь HTTP-запрос обязательно оставался активным в момент отмены. Мы демонстрируем отмену одной составной операции, включающей сетевое чтение и учебную задержку завершения. В инструментах сеть может закончиться быстрее, а результат всё ещё не применён.

Ручное наблюдение и ограничения

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

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

Отмена не восстанавливает прежнее значение формы автоматически. В нашем потоке новое действие уже задаёт новое желаемое состояние. Если пользователь нажимал отдельную кнопку «Отмена» без нового выбора, потребовалось бы определить, какой статус и какие карточки должны остаться. Эта продуктовая политика не следует автоматически из API контроллера.

Когда прекращение работы уже не помогает

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

Обратите внимание на замыкание функции задержки. Оно хранит ссылку на конкретный сигнал и конкретный таймер. При нормальном завершении слушатель отмены удаляется; при отмене удаляется таймер и тот же слушатель. Иначе многократные попытки могли бы оставлять лишние связи. Наш небольшой пример не содержит диагностического замера памяти, но код явно показывает, какой ресурс создаётся и где освобождается.

Отмена не является ограничением длительности по умолчанию. В текущем снимке она возникает при замене выбора, а не после истечения заданного времени. Если нужен тайм-аут, придётся определить отдельную причину и сообщение: «пользователь выбрал другое» и «источник слишком долго отвечает» требуют разных действий. Также нельзя считать искусственные шестьсот миллисекунд тайм-аутом: это только воспроизводимое окно для опыта.

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

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

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

Оглавление курса