Code Ready | Frontend
Почему старый fetch-запрос может перезаписать новый?
Асинхронные операции завершаются независимо от порядка запуска. Поэтому в интерфейсах, где новый запрос делает предыдущий результат неактуальным, может возникнуть race condition.
Например, пользователь быстро продолжает вводить поисковый запрос. Запрос для более длинной и актуальной строки может завершиться раньше предыдущего. Если каждый ответ безусловно передавать в renderResults(), более старый результат в итоге перезапишет актуальный:
search.addEventListener('input', async event => {
const query = encodeURIComponent(event.target.value);
const response = await fetch(`/api/search?q=${query}`);
renderResults(await response.json());
});
Проблема здесь не в fetch() как таковом, а в отсутствии контроля актуальности асинхронной операции: любой завершившийся запрос получает возможность изменить состояние интерфейса.
Если устаревшие запросы больше не нужны, для fetch() их можно отменять через AbortController:
let controller;
search.addEventListener('input', async event => {
controller?.abort();
controller = new AbortController();
try {
const query = encodeURIComponent(event.target.value);
const response = await fetch(`/api/search?q=${query}`, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const results = await response.json();
renderResults(results);
} catch (error) {
if (error.name !== 'AbortError') {
console.error(error);
}
}
});
При следующем вводе abort() переводит предыдущий AbortSignal в состояние aborted. Связанная с ним fetch-операция прерывается, поэтому её устаревший результат не попадёт в renderResults().
AbortSignal нельзя использовать повторно после отмены: он остаётся в состоянии aborted. Для следующей операции необходимо создать новый AbortController:
controller.abort();
controller = new AbortController();
Важно понимать границу такой отмены. abort() прерывает клиентскую fetch-операцию и чтение тела ответа, но не гарантирует прекращение обработки запроса на сервере. К моменту отмены запрос уже мог быть отправлен, а сервер мог начать его обработку.
AbortController и debounce также решают разные задачи. debounce уменьшает количество запускаемых запросов, откладывая выполнение до паузы во вводе. AbortController позволяет прервать клиентскую операцию, которая уже была запущена, но перестала быть актуальной. В интерактивном поиске эти механизмы можно использовать совместно.
При этом отмена — не единственный способ защиты от устаревших результатов. Если операцию нельзя или не требуется отменять, актуальность результата можно проверить непосредственно перед изменением состояния:
let requestId = 0;
search.addEventListener('input', async event => {
const id = ++requestId;
const query = encodeURIComponent(event.target.value);
const response = await fetch(`/api/search?q=${query}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const results = await response.json();
if (id === requestId) {
renderResults(results);
}
});
Здесь каждый запрос получает монотонно возрастающий идентификатор. Изменить интерфейс может только результат последней запущенной операции. Даже если более старый запрос завершится позже, его результат будет проигнорирован.
🔥Основной принцип шире конкретного API: результат асинхронной операции должен изменять состояние только до тех пор, пока эта операция остаётся актуальной.
📣 Code Ready | #практика
27 · 1.8K ·