Обзор
Throttle
и Debounce
решают проблемы оптимизации.
Throttle
– пропускает вызовы функций с определенной частотой.
Схема Throttle & Debounce:
Примеры использования Throttle:
1) Если пользователь изменяет размер окна браузера и нам нужно изменить содержимое сайта.
Без оптимизации происходит следующее. При каждом событии изменения размера окна вызывается обработчик события изменения размера окна. Поэтому если пользователь, например, изменяет размер окна в течение 10 секунд, то могут произойти 100, 200 и так далее событий, которые нам нужно обработать.
2) Показывать пользователю процент прокрутки страницы. Когда пользователь прокручивает страницу, происходят события scroll
, которые нам нужно обработать. С помощью throttle
мы можем уменьшить количество обрабатываемых событий прокрутки, задав временной интервал.
Примеры использования Throttle:
1) Обработка данных поискового запроса пользователя.
Когда пользователь вводит поисковый запрос, ему предлагаются варианты поиска. Это происходит следующим образом.
При изменении текста, введенного пользователем, на сервер отправляется запрос, в который мы передаем уже напечатанные символы. Затем мы получаем от сервера ответ с возможными вариантами поискового запроса и показываем их пользователю.
Каждый раз, когда пользователь меняет текст, вызывается обработчик события, в котором отправляется запрос на сервер.
Чтобы оптимизировать количество запросов, отправляемых на сервер, мы используем Debounce
.
Когда текст изменяется пользователем, использование Debounce
позволяет нам создать таймер, например, на 1 секунду. Если пройдет 1 секунда и пользователь не изменит текст во второй раз, то будет вызван обработчик события и запрос будет отправлен на сервер. Если пользователь изменяет текст второй раз за 1 секунду, то первый таймер сбрасывается и создается новый таймер снова на 1 секунду.
Таким образом, если пользователь редактирует текст поиска быстро (менее 1 секунды), то запрос будет отправлен на сервер только один раз, после того как пользователь перестанет набирать текст.
2) Отправка аналитических данных на сервер. Например, пользователь перемещает мышь по сайту, мы записываем координаты мыши в массив, после чего Debounce
позволяет нам отправить информацию о перемещении мыши клиента на сервер только после того, как клиент перестанет двигать мышью.
Итак, в этой статье я покажу вам, как использовать Throttle
и Debounce
в приложениях React.
Шаг 1 – Шаблон приложения
Создайте шаблон приложения с помощью create-react-app
и запустите его:
npx create-react-app throttle-debounce
cd throttle-debounce
npm start
Заменяем содержимое файла App.css
нашими стилями:
body {
display: flex;
justify-content: center;
width: 100%;
}
h1 {
text-align: center;
margin: 0.5rem 0;
}
.l-scroll {
overflow-y: scroll;
overflow-x: hidden;
width: 380px;
height: 200px;
margin-top: 0.5rem;
}
.scroll-content {
width: 100%;
background-color: bisque;
padding: 0 1rem;
}
.l-scroll::-webkit-scrollbar {
width: 10px;
height: 8px;
background-color: darkturquoise;
}
.l-scroll::-webkit-scrollbar-thumb {
background-color: blueviolet;
}
Заменим содержимое файла App.js
нашим шаблоном приложения:
import './App.css';
import { useMemo } from 'react';
function App() {
return (
<>
<h1>Throttle & Debounce</h1>
<div className="l-scroll">
<div className="scroll-content">
<TallContent />
</div>
</div>
</>
);
}
// High height scrollable content
function TallContent(){
const dataElements = useMemo(() => {
const genData = [];
for(let i=1; i<=200; i++){
genData.push(
<div key={i}>Line: {i}</div>
);
}
return genData;
}, []);
return(
<>
{dataElements}
</>
);
}
export default App;
Шаблон приложения готов, перейдем ко второму шагу – обработчику обычного события прокрутки.
Шаг 2 – хендлер обычных событий
Здесь мы добавим обработчик обычных событий для событий scroll
и подсчитаем количество вызовов этого обработчика, когда пользователь прокручивает элемент страницы.
Добавим состояние количества вызовов обработчика событий в компонент App
:
// At the beginning of the file
import { useState, useMemo } from 'react';
// Inside the App component
const [scrollHandleCount, setScrollHandleCount] = useState(0);
Затем добавим обработчик события прокрутки, для этого добавим атрибут onScroll
к элементу под заголовком h1
:
// Before
<div className="l-scroll">
...
</div>
// After
<div className="l-scroll" onScroll={handleScroll}>
...
</div>
Мы также добавим функцию для обработки события handleScroll
в компонент App
:
function handleScroll(){
handleUsualScroll();
}
Внутри функции handleScroll
мы поместили функцию, в которой будет обрабатываться обычное событие. Давайте добавим эту функцию в наш компонент App
:
function handleUsualScroll(){
setScrollHandleCount((prevState) => {
return ++prevState;
});
}
Осталось только показать пользователю состояние счетчика, для этого добавим строку кода под заголовком h1
:
<span>
Usual scroll handle count: {scrollHandleCount}
</span>
<br />
Теперь, при прокрутке элемента на странице, мы должны видеть количество вызовов функции handleUsualScroll()
.
Полный код компонента App
на данный момент:
function App() {
const [scrollHandleCount, setScrollHandleCount] = useState(0);
return (
<>
<h1>Throttle & Debounce</h1>
<span>
Usual scroll handle count: {scrollHandleCount}
</span>
<br />
<div className="l-scroll" onScroll={handleScroll}>
<div className="scroll-content">
<TallContent />
</div>
</div>
</>
);
function handleScroll(){
handleUsualScroll();
}
function handleUsualScroll(){
setScrollHandleCount((prevState) => {
return ++prevState;
});
}
}
Шаг 3 – Обработчик событий с помощью Throttle
Обработчик событий Throttle
в нашем случае должен вызывать увеличение счетчика scrollThrottleHandleCount
, пропуская вызовы для увеличения счетчика через определенные промежутки времени.
Для реализации наших планов нам нужен таймер, при запуске которого состояние Throlle
переходит в In progress
. При этом, если состояние In Progerss
, то обработка пользовательских событий (прокрутка элемента страницы) пропускается.
Как только сработает таймер, состояние Throttle
изменится на Not in progress
, что означает, что наш обработчик снова будет обрабатывать пользовательские события. Таким образом, пользовательские события пропускаются через заданный промежуток времени.
Мы реализуем вышеописанное:
// Add useRef to store inProgress state
import { useState, useRef, useMemo } from 'react';
Далее, в компоненте App
добавьте счетчик вызова обработчика событий с состоянием Throttle
и ref
для хранения состояния inProgress
:
// Number of event handler calls with Throttle
const [
scrollThrottleHandleCount,
setScrollThrottleHandleCount
] = useState(0);
// Keeping the state in progress
const throttleInProgress = useRef();
Здесь важно отметить, что throttleInProgress
является частью побочного эффекта, связанного с таймером, что означает, что мы будем хранить состояние в объекте ref
, поскольку useRef
возвращает объект, существующий на протяжении всего жизненного цикла компонента, а при изменении свойства current
объекта, возвращаемого useRef
, в отличие от useState
, нет дополнительного компонента рендеринга.
Теперь добавим сам обработчик события с Throttle
в компонент App
:
function handleThrottleScroll(){
// If the state is inProgress - exit the function,
// skip event processing
if(throttleInProgress.current){ return; }
// Set inProgress to true and start the timer
throttleInProgress.current = true;
setTimeout(() => {
// Increment the throttleHandleCount
// state by one
setScrollThrottleHandleCount((prevState) => {
return ++prevState;
});
// Set inProgress to false, which means
// that setTimeout will work
// again on the next run
throttleInProgress.current = false;
}, 500);
}
Осталось 2 простых шага: добавить отображение состояния счетчика с Throttle
для пользователя и добавить handleThrottleScroll()
к handleScroll()
:
// After heading h1
<span>
Throttle scroll handle count: {scrollThrottleHandleCount}
</span>
// In the handleScroll() function after handleUsualScroll();
handleThrottleScroll();
В результате мы получим:
Обычный обработчик событий вызвал бизнес-логику приложения 181 раз, а с Throttle
только 9.
Полный код компонента App
с Throttle
:
function App() {
const [scrollHandleCount, setScrollHandleCount] = useState(0);
const [
scrollThrottleHandleCount,
setScrollThrottleHandleCount
] = useState(0);
const throttleInProgress = useRef();
return (
<>
<h1>Throttle & Debounce</h1>
<span>
Usual scroll handle count: {scrollHandleCount}
</span>
<br />
<span>
Throttle scroll handle count: {scrollThrottleHandleCount}
</span>
<br />
<div className="l-scroll" onScroll={handleScroll}>
<div className="scroll-content">
<TallContent />
</div>
</div>
</>
);
function handleScroll(){
handleUsualScroll();
handleThrottleScroll();
}
function handleUsualScroll(){
setScrollHandleCount((prevState) => {
return ++prevState;
});
}
function handleThrottleScroll(){
if(throttleInProgress.current){ return; }
throttleInProgress.current = true;
setTimeout(() => {
setScrollThrottleHandleCount((prevState) => {
return ++prevState;
});
throttleInProgress.current = false;
}, 500);
}
}
Перейдем к последнему шагу – реализации обработчика события Debounce
.
Шаг 4 – Обработчик событий с Debounce
Debounce
в нашем примере задерживает увеличение счетчика scrollDebounceHandleCount
до тех пор, пока не пройдет определенное количество времени с момента последнего вызова обработчика события***.
Добавим состояние количества вызовов обработчика события с Debounce
, ref
для хранения идентификатора таймера в компоненте App
:
const [
scrollDebounceHandleCount,
setScrollDebounceHandleCount
] = useState(0);
const timerDebounceRef = useRef();
Затем мы покажем пользователю количество scrollDebounceHandleCount
и добавим наш метод handleDebounceScroll()
в handleScroll()
:
// After h1
<span>
Debound scroll handle count: {scrollDebounceHandleCount}
</span>
// In handleScroll() function
handleDebounceScroll();
Осталось написать функцию handleDebounceScroll
:
function handleDebounceScroll(){
// If the timer ID is set, reset the timer
if(timerDebounceRef.current){
clearTimeout(timerDebounceRef.current);
}
// We start the timer, the returned timer ID
// is written to timerDebounceRef
timerDebounceRef.current = setTimeout(() => {
// Increasing the counter for the number of
// executions of the business logic
// of the application with Debounce
setScrollDebounceHandleCount((prevState) => {
return ++prevState;
});
}, 500);
}
В результате счетчик Debounce
увеличивается только тогда, когда пользователь останавливает прокрутку элемента страницы на время большее или равное 500 миллисекундам:
Полный текст компонента App
:
function App() {
const [scrollHandleCount, setScrollHandleCount] = useState(0);
const [
scrollThrottleHandleCount,
setScrollThrottleHandleCount
] = useState(0);
const [
scrollDebounceHandleCount,
setScrollDebounceHandleCount
] = useState(0);
const throttleInProgress = useRef();
const timerDebounceRef = useRef();
return (
<>
<h1>Throttle & Debounce</h1>
<span>
Usual scroll handle count: {scrollHandleCount}
</span>
<br />
<span>
Throttle scroll handle count: {scrollThrottleHandleCount}
</span>
<br />
<span>
Debound scroll handle count: {scrollDebounceHandleCount}
</span>
<div className="l-scroll" onScroll={handleScroll}>
<div className="scroll-content">
<TallContent />
</div>
</div>
</>
);
function handleScroll(){
handleUsualScroll();
handleThrottleScroll();
handleDebounceScroll();
}
function handleUsualScroll(){
setScrollHandleCount((prevState) => {
return ++prevState;
});
}
function handleThrottleScroll(){
if(throttleInProgress.current){ return; }
throttleInProgress.current = true;
setTimeout(() => {
setScrollThrottleHandleCount((prevState) => {
return ++prevState;
});
throttleInProgress.current = false;
}, 500);
}
function handleDebounceScroll(){
if(timerDebounceRef.current){
clearTimeout(timerDebounceRef.current);
}
timerDebounceRef.current = setTimeout(() => {
setScrollDebounceHandleCount((prevState) => {
return ++prevState;
});
}, 500);
}
}
Подписывайтесь на блог, ставьте лайки, добавляйте в закладки.
Не забывайте про единорогов.
Спасибо за внимание!