Стварэнне інструменту debounce з нуля: таймеры, аргументы і іншы элементы.
Створыце функцыю debounce, якая можа быць викорыстаная знову, шаг за шагам; дазнаецеся, чырвону яна перадае аргументы і калі замест таго варта выкарыстоўваць throttle, а таксама як ёю скорыстацца разам з AbortController.
Крыніцы: Debouncing in JavaScript: How to Stop Sending Useless API Requests
Поле пошуку, якое звяжыцца з серверам па кожны натиск клавішы, ператварае адзін напісаны слова на масу запитоў, большая частка з якіх відхіляецца. Раўленне — это маленькі інструмент пад назвай debounce. Хутчэй вы яго реалізуеце, зрозумееце кожную лінію коду, выберэце между методамі debounce і throttling, а таксама скомбінуеце яго з анулюванням запитоў, каб процес пошуку працаваў эфективна пад час рэальнага набірання тексту.
Як натыск клавіш прыводзіць да марнавання запитоў
Уявіце корыстніка, які шукае певную фрамворк. Значэнне, яке вводзится, зменшаецца на адну літеру за раз:
r
re
rea
reac
react
Якщо кожная змяна спрычыняе вызов API, сервер обробляе пяць запитоў за адну заявку. Болей конкрэтны пошук толькі пагаршае ситуацыю:
r
re
rea
reac
react
react j
react js
react js d
react js de
react js dev
Это дзесять запитоў, і толькі апошній відображае тое, што хацеў корыстнік; рэшта все равно спрацоўвають, выкарыстоўваючы пропускную здатнасць, час сервера і ліміт колькасці запитоў.
Ідея, стоячая за методам debounce
Функцыя дэбаунсінгу адкладае выклік функцыі пакуль не прыйдзе назначаны час без дадзеных ўжо выклікаў: пацекайце, калі корыстнік заканчыць, тады выконайце задачу.
Спакуймо, што выклік пошуку адкладваецца на 500 мс, і корыстнік зноў вводзіць тыя ж пяць симвалаў:
r
re
rea
reac
react
Кожны натык на клавішу пачынае занова лічэнне часу, таму пакуль трывае ввод, нічога не адправляецца. Пасля 500 мс тышы адправляецца адна запчатка:
react → API request
Цикл прыняцтва рашэння праз кожны выклік:
Typing
↓
Wait 500ms
↓
Did user type again?
↓
Yes → Reset timer
↓
No → Execute function
Поле пошуку без жадной захоўкі
Пачніце з простага элемента вводу:
<input id="search" placeholder="Search..." />
З’явіце сабе кансэкт да яго ў JavaScript:
const searchInput = document.querySelector("#search");
Потым прыўяжыце аслухальнік input, який перадае значэнне функцыі пошуку, якая толькі логавае, што дапамагае пасцярогваць, як часта ёй выклікаюцца:
searchInput.addEventListener("input", (event) => {
searchProducts(event.target.value);
});function searchProducts(query) {
console.log("API request:", query);
}
Тепер спакуймо, што хтось вводзіць назву продукту:
iphone
Эвэнт input запускаецца адна раз на кожны симвал, прыблізна па такой сэрый:
i
ip
iph
ipho
iphon
iphone
Такім чынам працоўнік функцыі выкананы шасць разоў і запішае шасць запытак:
API request: i
API request: ip
API request: iph
API request: ipho
API request: iphon
API request: iphone
Для рэальнага API это значыць шасць раунд-трыпов, тады как корыстніку важна толькі:
iphone
Рэалізацыя функцыі debounce за калькі ліній
Этая утилітая — це клаузура: яна прымае функцыю і час затрымкі, а потым вяртае обгортку, яю трэба вызваць замест адной функцыі:
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
Зберагаць адны таймер на кожную функцыю з debounce
let timer;
timer знаходзіцца ў клаузуре, таму кожны вызов обгорткі бачыць тое жа значэнне; так адны вызов можа знайсці, што паканальны вызов яшчэ чакае. Кожны вызов debounce атрымлівае свой сабэстоячы таймер.
Анулюваць чакаючы вызов
clearTimeout(timer);
Кожны раз запуск анулюе чакаючы выклік, які ўсё еще існуе. Перадача значэння undefined функцыі clearTimeout пад час першага запуску не мае наследкав.
Запланаваць новы выклік
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
Новы таймаут запускае первоначальную функцыю пасля заканчэння періоду чакання. Пры значэнні 500 мс быстрые выклікі працююць так:
Call
↓
500ms timer starts
Call again
↓
Old timer cancelled
↓
New 500ms timer starts
Call again
↓
Old timer cancelled
↓
New 500ms timer starts
Функцыя запускаецца толькі пасля таго, як пройдзе цэлы період у 500 мс без іншага выкліку, і тады дзейнуюць апошнія параметры.
Падключэнне гутарыкі да поля пошуку
Заменіце просты обробнік на версію з дебаусінгам (это заменяе паказаную раней декларацыю searchProducts, адколі обе вжываюць тое ж назва):
const searchProducts = debounce((query) => {
console.log("API request:", query);
}, 500);
searchInput.addEventListener("input", (event) => {
searchProducts(event.target.value);
});
Калі корыстнік уводзі пяць вядомых симвалаў:
r
re
rea
reac
react
Кожны натык на клавішу анулюе пярэдні таймер, і ў канцы канчыцца період тышы:
react
↓
500ms
↓
API request
Рэзультатам яе адна запытка API замест пяці.
Чаму кэшэйвер перадае аргументы
Два элемента здаюцца неабяжнымі, але яны робяць прыемнік павторна выкарыстоўваным. Першы з іх — параметр rest:
return function (...args)
Другі элемент — спосаб вызвання аднаго з функцый:
fn.apply(this, args);
Падтрымка любага колькасці аргументаў
Універсальны прыемнік дэбаунсу не должен нічога прыпускаць аб формате функцый, якія ён кэшуе. Ён можа кэшаваць функцыю для пошуку з адним аргументам:
const search = debounce((query) => {
console.log(query);
}, 500);
альбо функцыю, якая прыймае два значэння:
const calculate = debounce((a, b) => {
console.log(a + b);
}, 500);
Сбор усьго за дапамою синтаксі rest і ўсуненне гэтага матерыялу дазволяе обом функцыйням працаваць:
...args
Зберэжчык контекста вызвання
Тая ж лінія таксама перадае this:
fn.apply(this, args);
Рамкавае элемента ўособлівае ся як звычная функцыя, таму яна прымеўа this вызывача, напрыклад об’ект, калі функцыя з адстрэйлінгам зберагаецца як метод. Функцыя-стрэлка ўнутры setTimeout не мае своего this, таму яна перэймае this рамкавага элемента, а apply яго перадае. Адпаведальны за пошук код ніколи гэтага не зазначае, але спяльны інструмент, які выключае контэкст, вызывае плутанні кашы в коде, заснованам на об’ектах. Заўважыце, што функцыя-стрэлка, паслаўшаяся як fn, незалежна ад гэтага застаецца зі своім лексычным this.
Дзе адстрэйлінг дапамагае
Гэты патэрн падходзіць для будзь-якага з’явлення падзеі ў шырокіх інтервалах часу, калі значны толькі фінальны стан.
Прамовы для пошуку
Простае рашэнне адправляе запыт за кожны натиск клавішы:
User types → API request
У режыме з адстрэйлінгам спачатку чакаюць на паузу:
User types
↓
Wait
↓
User stops
↓
API request
Змена размеру вікна
Прыткія края вікна ствараюць безперывныя запускі змагання resize, і кожны раз запускаецца прымкі выконвае даскладную роботу:
window.addEventListener("resize", () => {
expensiveCalculation();
});
Гэта можа значыць сотні запускаў за адну прыткую. Якщо робота запакавана, яна выконваецца толькі пасля таго, як змагання вярнуцца на свае месца:
const handleResize = debounce(() => {
expensiveCalculation();
}, 300);
window.addEventListener("resize", handleResize);
Автазберэжчык
У редакторах техніка дебаунсінга ператварае серыю змян у аднае зберэжчык пасля таго, як корыстнік зупініцца:
User types
User types
User types
User types
...
User stops
↓
Save document
Увага: тэхніка дебаунсінга ніколи не прымае дзейства, калі корыстнік пішча без перапынку, а незабавна зберэжчык трапляецца загубленай, якщо сторанка закрыцца. Редакторы зазвычай задаюць максимальны час чакання або здыяваюць зберэжчык пад змаганням visibilitychange.
Дебаунсінг проты тротлінгу
Этыя два падходы легка сплутваць. Дебаунсінг чакае, пакуль дзейства не зупіняцца, а потым выконваецца аднае раз:
Typing → Typing → Typing → STOP
↓
Execute
Тротлінг выконвае функцыю ў максымум аднае раз на фіксаваны інтэрвал, незалежна ад таго, скількі змагань прыйшло:
Event → Execute
↓
100ms
↓
Ignore
↓
100ms
↓
Execute
Выконанне спачатку або напрыжку додае нюансаў, але гэта раззялленне пакрывае большасць случаяў.
Функцыя debounce не выправляе застарэлых адпаведзей
Функцыя debounce обмежвае колькість запускаемых запитоў, а не тое, што вадзіцца з тыми, якія вже ў парадку выпалення. Падазроўваючы, што корыстнік настацькі затрымліваеся, каб запит быў аправлены, а потым скорага заставіць запит:
User searches: react
↓
Request A
User changes search: react native
↓
Request B
Якщо запит B вярнёцца раней за запит A, старэйшая адпаведзь прыбудзе ў пасляднюю чаргу і заменіць новейшыя рэзультаты. Тады інтэрфейс пакажае рэзультаты для:
react
навет як увод тепер гаворыць:
react native
Выправленне — аббяць застарэлыя запиты за дапамою AbortController, які працуе пад час функцыі debounce:
Debouncing
↓
Reduce unnecessary requests
↓
AbortController
↓
Cancel outdated requests
Гэтыя супербегі дакладна расказаны ў як выправіць супербегі, якія не можа рашыць функцыя debounce.
Аб’еднанне двух слоёў
У гэтым падходзе выкарыстоўваецца controller на рэвэлі модуля. Кожны раз, калі запускаецца функцыя з адстрочкай, яна завершае паказаны раней запит, стварае новы controller і перадае яго signal функцыі fetch. Функцыя encodeURIComponent заходзіць спецыяльныя символы, а прычыну AbortError ігнаруецца, адтолькі як што гэта означае намеравана ануляцыя, а не адказчыню:
const searchInput = document.querySelector("#search");
let controller;
const searchProducts = debounce(async (query) => {
controller?.abort();
controller = new AbortController();
try {
const response = await fetch(
`/api/products?q=${encodeURIComponent(query)}`,
{
signal: controller.signal
}
);
const data = await response.json();
renderResults(data);
} catch (error) {
if (error.name !== "AbortError") {
console.error(error);
}
}
}, 500);
searchInput.addEventListener("input", (event) => {
searchProducts(event.target.value);
});
Адстрочка запобегае ад стварэння новага запита пасля кожнага натыку клавішы; AbortController зупіняе запиты, рэзультаты якіх больш не маюць значэння.
У прыметным варыянце таксама трэба праігнораваць порожнія запиты і пераканацца, чы ў response.ok є значэнне, прычым перад парсаванням, адтолькі як што fetch не відхіляе запиты з статусамі HTTP-памялкі.
Короткая модэль прыняцтва рашэння
Калі якісь змаганні выконваюцца парадульна, запытайце ся, чакі вам трэба рэагаваць на кожна з іх. Як ні, другы вопыт дапамагае прыняць рашэнне. Для вхідных дадзеных вам патрэбна заканчоўная значэнне, таму выкарыстоўваецца механізм debounce:
Input events
↓
Do I need every event?
↓
No
↓
Do I care about the final state?
↓
Yes
↓
Debounce
Для прасування вам патрэбны регулярныя апдэйты пад час выканання жеста, таму выкарыстоўваецца механізм throttle:
Scroll events
↓
Do I need every event?
↓
No
↓
Do I need periodic updates while scrolling?
↓
Yes
↓
Throttle
Узагальненне
Механізм debounce насправды служыць для кантролю таго, як часта выконваецца даскональна трыючая работа, і ўсё гэта складаецца з двух рэчэй:
clearTimeout(timer);
timer = setTimeout(() => {
fn();
}, delay);
Выкарыстоўваеце гэты механізм, калі падобныя змаганні выконваюцца быстра ў ряд:
input
resize
search
auto-save
Калькі пунктав, якія трэба запамятаць:
- Функцыю debounce трэба ствараць адназначна і вяртаць яе знову; стварэнне новай функцыі для кожнага змагання (או для кожнага рэенду React без
useMemoабоuseRef) прыводзіць да таго, што кожны вызов будзе мяць свой таймер, і гэта блокуее эфект. - Перадача
...argsіthisдапамагае зробіць гэтую функцыю безпечнаю для паўторнага выкарыстоўвання.
AbortController адпаведае за тое, чы можа завершыцца застарэлы запыт. Абодва методы трэба выкарыстоўваць для пошуку, які базуецца на сети.