Анулюванне застарэлых задаń у React: AbortController для змены стану
Дазнайце, чаму праўільная інформацыя не падае ў ваш интерфейс, а таксама як функцыя AbortController анулюе рэальны запит, і як яна дапамагае debounce і throttle у React.
Калі корыстнік змянюе сваё рашэнне быстрей, чым сеть рэагуе, кожны запрос, які вы вялікая часткачаса ўжо запусцілі, продовжыць працаваць і з радасцю запісае свой рэзультат на экран. У поле пошуку гэта значыць рэзультаты запита, які корыстнік абандонаваў; у плееры медыя – момант відображэння песні, якую ён ўжо прахадзіў. Чаканне чы абмежэння частоты запуску не рашаюць гэтай проблемы, таму што работа вже ў процесе. У этай статыцэ показана, як AbortController насправдзе анулюе такую работу, як яго інтегравацыя ў эфект React, а таксама як ён дапамагае разам з методамі debounce і throttle, а не заменяе іх.
Гонка: рэспансы прыходзяць у неправильнай парадку
Узьмем прыклад пошуку. Пользователь піскае „ni“, і надаёцца запит. Потым ён піскае „ke“, і надаёцца другі запит на пошук „nike“. Якщо сервер працюе повольней з першым, корачым запитам, яго адпаведзь надходзіць пасля другога і заменяе правильныя рэзультаты старымі.
Плеер аудыявання дэтальвае той жа прыклад, але з большымі наследкамі. Калі нажмаць на тракт, пачынаецца яго завантажэнне. Калі нажмаць на іншы тракт раней, чым першы будзе готавы, пачынаецца другое завантажэнне, і тады оба тракты конкуруюць за керування плеерам. На момент можа быць, што слухач учуе пропусканы тракт, паліця прогрэсу можа показваць некоректную трываласць, або два тракты можу прабаваць кераваць плеерам адночасна.
Функцыя debouncing тут не дапаможа. Яна затрымлівае пачатак выканання задачы, пакуль вхідны данны не стабілізуюцца; але яна нічага не робіць з запитам, які вже выкананы. Тут не хапае анулювання: паведамлення про тое, каб задача, яка вже запусцілася, зупінілася.
Што выходзіць з-пад контролу без анулювання
fetch, стрімы, завантажэнне медыя і абэранты з’явоў — усе гэта прыклады задач, якія продаюцься самыя пасля таго, як былі запусцаны. Калі няма можлівасці іх зупініць, часта выступаюць тры відэны негатыўных наследкав:
- Застарэлыя данні пераважаюць. Старэйшы адпаведны рэсурс завершае свою працу пасля новейшага і заменяе тое, што апісваецца на экране, або ў плэйеры прыпыняе сваю роботу.
- Марнаўванне рэсурсаў. Шырокасмуговая працэздатнась, акумулятар, CPU сервера і запыты CDN выкорыстоўваюцца для контэнту, які ніхто не будзе бачыць чы слухаць.
- Проблемы з інтэрфейсам. Індыкаторы, якія ніколі не зупіняюцца, хвильовы графік пясні, якая вже завершылася, або заголовак, які два разы заўважна мяніцца.
Класычным спосабам разв’язкі є наяўнасць захоўнага коду, такога як if (requestId !== latestId) return у кожнам калбэку, або тыхае ігнораванне рэзультата ў .then(). Гэта працюе толькі тады, калі кожны калбэк памятае пра пераканранне, а запит все равно завершыцца і заванесе своі байты. AbortController дагэнае да таго, што анулюе саму операцыю, а не толькі ваша заінтэресаванасць у ўсунуцымся рэзультате.
Базовы шаблон AbortController
Контролер адкрывае signal. Вы перадаеце гэты сігнал будзь-яму API, які яго приймае, а вызов abort() у контролеры паведамляе всіх власнікаў сігнала прыпыніць роботу:
const controller = new AbortController();
fetch("/api/tracks/123", { signal: controller.signal })
.then((res) => res.json())
.then((track) => loadIntoPlayer(track))
.catch((err) => {
if (err.name === "AbortError") {
// expected. they picked a different song.
return;
}
throw err;
});
// they skipped, or left the page, or closed the player
controller.abort();
Тры ўскладнення заслуговяюць на увагу. Перша – сігнал практыкуецца ў параметрах fetch, і самэ гэта дапамагае fetch з’ясавіць, што яго можна анулюваць. Друга – ануляванне прыводзіць да таго, што прагненне адхіляецца з адной разлукою пад назвай AbortError, нават якщо заголовкі вже прыбылі, а res.json() яшчэ чытае тэла. Трэця – блок catch спрыймае гэтую разлуку як звычны, прыемлівы выхід і практыкуець усё іншае зноў, таму справжнія неудачы не захопляюцца.
Уся гэта тэхніка стоіць на простым цыкле жыцця: адны кантролер на кожную мету. Калі корыстнік выбирае новую песню, трэба анулюваць стары кантролер і створыць новы для новага завантажэння. Кантролер не можа быць перазначаны пасля анулявання, таму ўжыванне яго для разных запыткаў негайна анулюе будучую работу.
Якщо вы вызываете abort(reason) з власной прычынай, fetch адзначае памылку з той прычынай узаместо стандартнага AbortError. У такім случыку пераконтрольванне controller.signal.aborted ўсё ж адпаведальнейшы спосаб разлічыць намерована ануляванне ад справжняй памылкі.
Паўязка анулявання з эфектам React
У React натуральны месца для вызову абанавання — це функцыя чысткі эфекта. Калі значэнне, яке керуе запытам, праграмавана з пропсоў або статау, запыт должен існаваць ровна столькі, сколькі і гэта значэнне:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/tracks/${trackId}`, { signal: controller.signal })
.then((res) => res.json())
.then(setTrack)
.catch((err) => {
if (err.name === "AbortError") return;
setError(err);
});
return () => controller.abort();
}, [trackId]);
Калі змінюецца trackId, React выкананаў чыстка праз пярэдній рендер прычыму запуску наступнага эфекта, таму запрос да старай песні абрываецца, і толькі тады пачынаецца новы. Калі плейер адключаецца, выкананая тая ж самая чыстка, што значыць, ўпоследжанае адказванне ніколі не можа вызваць setTrack у компоненте, які ўжо не існуе.
Пад час разработкі ў режыме Strict Mode React намеравана адключае, чысткае і зноў выкананае эфекты адной раз. За дапамогою гэтага патерна вы пабачыце адні запрос, які быў абрываны, у панелі сеті; гэта чыстка, якая выкананае свою роботу, а захід AbortError не дазволяе яму праказвацца як адказка.
Той самы сігнал можа кераваць больш чым толькі fetch. Стрымы прыймаюць яго, бібліятэкі, якія абгружаюць XHR, часта таксама яго прыймаюць, а addEventListener прыймае параметр signal, які адключае слухальнік, калі сігнал завершаецца. Аднак ёсць адна прычына для праблем з плеерамі: звычайны элемент <audio> не прыймае сігнал. Чырагчыць буфарацыю файла, які быў прыняты, трэба таксама ачыстыць або заменіць його src у функцыі чысткі.
Debounce, throttle і abort рашаюць разныя проблемы
Этыя тры інструменты часта з’яўляюцца разам пад полем пошуку і кантролламі плеера, таму іх лёгка сплутаць. Кожны з яных дзейнае у разны момант:
- Debounce чакае, пакуль корыстнік не зупініцца, а потым выкаанае дзеянне адной раз. Якшо швыта запісаць „n-i-k-e“, пасля последнага натыку клавішы можа быць выдана толькі адна запитоўка.
Іншымі словамі, debounce і throttle адпаведна вяршаюць, калі можна запускаць новую задачу, а abort вяршае, чы можа продавацца задача, якая вже запусцілася.
Поле пошуку, якое даўае адчуття чутлівасці, зазвычай викорыстоўвае два з іх. Debounce не дазволяе выканаваць запит пасля кожнага нажатая клавішы, а abort гарантуе, што запит, які вже быў адправлены, не зможа праз час вернуцца і перазапісаць новыя рэзультаты. У спецыяльной стацыі на условіях канкурэнцыі, якія debounce не можа рашыць у інтерфейсах пошуку дакладна розглядаецца гэты случай.
Ігрок таксама дэйствуе за тым жа принцыпам. Можна обмежыць частоту нажатых на кнопку „Наступны“ крок, ўбліжчы, што адзін занадта актыўны корыстувач не зможа запусціць 20 завантажэнняў за 200 мс, але такое обмежэнне нічога не скасоўвае; яно толькі расспаковывае новыя запускі. Вам усё равно трэба анулюваць завантажэнне, якое вяліся ўжо.
Кожны з эых методаў залишае прычыну для проблем: функцыя debounce даўжа дазволяе медленнаму запиту, адправленаму рана, прыйсці пазней, а сама ануляцыя яшчэ ўсё равно перавантажвае сервер.
Аналіз ситуацыі пад час прослухоўвання плейліста
Разглядзім, што адбываецца ў плееры для аудыя, калі хтось пераходзіць між трактамі плейліста быстрэй, чым сеть можа адпаведаць.
Корыстувач нажымае на тракт А. Дапрыга прасіла метаданы, можа, падпісаны URL да файла, атракцыяныя зображэння і графік хвалёў, і аудыя пачынае бафаруванне. Яшчэ перш чым усё гэта завершыцца, ён нажымае на тракт Б, а потым на тракт В.
Без ануляцыі ўсе, што запускалася для тракта А, продовжае прыходзіць:
- Метаданы тракта А прыходзяць і задаюць назву.
Пры анулюванні натыканне на B завершае ўсё, што было запусцана ў імя A. Џэдыныя рэакціі, якім разрашаецца змяніць аудыйны элемент, заглавчык і графік хвалей, належаць той песні, якая выбрана ў тым моманті. Якщо зноў нажмаць «Праскочыць», робота B будзе анульвана, а C будзе продаважвацца. Калі закрыць плеер, адбываецца чыстка, і нічога не застаёцца для запісу ў компонент, які вярнуўся.
Адзін кантроллер дзеўаецца ўсім запытам для выбору, і аднае вызванне abort() знішчае цэлую групу.
Рэзультатам яе ўжыцця є тое, што інтэрфейс завжды прадстаўляе толькі текущую намеру пользователя.
Той самы прыем у большых застаўкі
У большых застаўках гэя ідея прыменяецца всюды:
- Typeahead і фільтры. Новы запыт анулюе пярэдні, што ў ролі плейліста выступае сукупнасць рэзультатаў пошуку заместо песней.
- Рутаванне на стороне кліента. Калі пользователяцель пакідае сторунку перад тым, як вярнуцца ўсі даны, фрэймворкі і бібліятекі дадзеных, такія як Next.js, Remix, TanStack Query і SWR, можуць анулюваць чакаючую на выкананне роботу па навігацыі; у сутнасці, гэта таксама сигнал анулявання. Пераканайцеся ў дасведчэннях кожной бібліятекі, калі саме яна анулюе запыт і чы роўным чыном перадае сигнал вашаму засобу запытання.
controller.abort(), якая знаходзіцца за ціёю кнопкай.Галоўныя выводы
- Функціі debounce і throttle кантролююць момент пачатку работы; толькі можласць абрыцья вядома, чы будзе завершана ўжо пачатая работа.
- Ствараць аднаго
AbortControllerна кожную задачу, перадаваць йогоsignalу всіх месцах, дзе ведаць работу, і заменяць яго замест таго, каб практыкаваць перызнаўленне. - Разглядаць
AbortErrorяк нормальны выхід і перрабрасваць іншыя паказанні пра бяды. - У React трэба абаратаць выконанне эфекту ў функцыі cleanup, каб змена вводных дадзенняў чы ад’язванне компонента автаматычна анулявалі чакаючыя запиты.
- Памятайце, да чаго не дасягае сігнал, напрыклад, сама процэс заробкі медыя-элемента, і явна абаратаць такія процесы.
Ануляванне сама па сабе не зробіць інтэрфейс умалейшым, але гарантуе, што запиты з вяршынкі дня не зможу канфліктуваць з запитамі сёння. Калі вы вже бачылі, як такі канфлікт практыкуецца на экране чы через колак, стварэнне сігнала для кожнага запиту, які мае право апдэйтаваць UI, становіцца прыкметай, яку варта падтрымаць.