Состояние гонки
При разработке пользовательских интерфейсов мы часто сталкиваемся с обработкой каких-либо асинхронных операций. Зачастую в качестве таких операций выступают сетевые запросы. Сеть зависит от множества разных вещей, что делает её весьма непредсказуемой. Предлагаю рассмотреть на примерах, что я имею в виду.
Гонки клонов
Устроим гонки покемонов: выставим на дуэль Пикачу и… ещё одного Пикачу! Почему бы и нет?
Вопрос: в каком порядке выведутся цифры в консоли (прямо как на собеседовании) какой из двух Пикачу придёт к финишу первым?
На самом деле на этот вопрос нет однозначного ответа и результаты этого эксперимента могут отличаться в зависимости от стабильности интернета, реализации бекенда или даже браузера. Что же нам даёт эта информация?
Что не так с моим поиском?
Рассмотрим довольно распространённый сценарий с поиском, результаты которого перезапрашиваются при каждом изменении пользователем поисковой строки. В случае с React можно написать следующий код:
Функция fetchData
в данном примере будет эмулировать нестабильность сетевого запроса:
Пример может показаться довольно бессмысленным, но зато он максимально наглядно проиллюстрирует проблему:
Обратите внимание на то, что происходит с результатом во время ввода и что мы получаем в итоге.
Скорее всего вы уже догадались, в чём дело, но чтобы точно понять, с чем мы имеем дело, предлагаю рассмотреть последовательность происходящих событий на диаграмме:
По какой-то причине выполнение первого запроса заняло больше времени, чем выполнение второго, но его данные уже неактуальны. Такая ситуация называется
Самый простой способ решить эту проблему — просто проигнорировать результат неактуального запроса.
Выбрасываем просрочку
Можно просто создать переменную isIgnored
и присваивать ей значение true
в момент, когда запрос становится неактуальным:
Таким образом проблема действительно будет решена и приложение будет работать более предсказуемо в непредсказуемых условиях:
Посмотрим, что изменилось на диаграмме:
Теперь при каждом изменении searchString
мы просто начинаем игнорировать результат предыдущего активного запроса.
Изначальная проблема решена, но теперь получается, что браузер тратит ресурсы на бесполезное сетевое взаимодействие. Можно ли как-то сказать самому браузеру, что результат запроса нам больше не нужен, чтобы он отменил его?
Экономим трафик
На самом деле создание переменной isIgnored
— довольно кустарный способ решения проблемы, ведь в браузере есть более мощный встроенный механизм —
Концептуально ничего не изменилось, поскольку AbortController
используется только нашим кодом, а функция, осуществляющая запрос, ничего о нём не знает.
Чтобы увидеть всю мощь AbortController
, нам понадобится использовать функцию, которая умеет с ним работать. К счастью обычный fetch
как раз умеет это делать — он принимает сигнал отмены (signal
) в качестве одного из необязательных параметров:
Внутри браузер подпишется на событие abort
у сигнала и прервёт запрос, когда событие произойдёт:
Убедимся, что это действительно работает:
В DevTools мы увидим нечто подобное:
Победа! Браузер физически отменил неактуальные запросы и не тратит на них трафик пользователя.
Заключение
Асинхронные операции бывают непредсказуемыми, поскольку физически могут выполняться параллельно основному потоку, поэтому очень важно следить за актуальностью данных, которые они нам возвращают. Способы решения проблем довольно простые, поэтому достаточно держать в голове возможность возникновения гонки при написании асинхронного кода.