Throttle: примеры (JAVASCRIPT)
throttle(func: Function, wait: Number): FunctionФункция throttle (дроссель) ограничивает частоту выполнения другой функции. Она гарантирует, что функция будет выполняться не чаще одного раза в указанный промежуток времени, даже если события, ее вызывающие, происходят значительно чаще.
Назначение и принцип работы
Использование throttle актуально при обработке часто генерируемых событий, таких как scroll, resize, mousemove или input в реальном времени. Это позволяет снизить нагрузку на браузер и повысить общую производительность веб-приложения.
В отличие от debounce, которая откладывает выполнение и ждет паузу, throttle позволяет функции выполняться регулярно, но с заданным минимальным интервалом. Если в течение этого интервала происходит новый вызов, он либо игнорируется, либо планируется на момент после окончания интервала (в зависимости от реализации).
Аргументы и возвращаемое значение
Классическая реализация принимает два основных аргумента:
- func (Function): Оригинальная функция, вызов которой требуется дросселировать.
- wait (Number): Временной интервал в миллисекундах, определяющий минимальную задержку между последовательными выполнениями функции.
Дополнительные, но часто встречающиеся параметры в расширенных реализациях:
- options (Object): Объект с настройками.
- leading (Boolean): Определяет, должна ли функция выполняться в начале интервала (при первом вызове). По умолчанию часто true.
- trailing (Boolean): Определяет, должна ли функция выполняться в конце интервала, если за это время были новые вызовы. По умолчанию часто true. Важно, что оба флага leading и trailing не могут быть одновременно false.
Возвращаемое значение: Функция throttle возвращает новую, обернутую (дросселированную) версию переданной функции. Эта новая функция при вызове сама управляет частотой выполнения оригинальной функции. Она также может иметь методы для отмены запланированного вызова или немедленного выполнения (например, cancel, flush).
Базовые варианты использования
Пример 1: Простой дроссель для события скролла.
function logScroll() {
console.log('Scroll!', new Date().toLocaleTimeString());
}
const throttledLog = _.throttle(logScroll, 2000); // Используем Lodash
window.addEventListener('scroll', throttledLog);
// При быстром скролле сообщение будет выводиться не чаще раза в 2 секунды.// Пример вывода в консоль: // Scroll! 12:05:30 // Scroll! 12:05:32 // Scroll! 12:05:34
Пример 2: Throttle с отключением leading edge.
function saveInput(value) {
console.log('Сохранение:', value);
}
const throttledSave = _.throttle(saveInput, 1000, { leading: false });
// Имитация быстрого ввода
throttledSave('A');
throttledSave('AB');
throttledSave('ABC');
// Функция выполнится только через 1 секунду после последнего вызова, получив значение 'ABC'.// (Пауза 1 секунда) // Сохранение: ABC
Пример 3: Throttle с отключением trailing edge.
function shoot() {
console.log('Выстрел!');
}
const throttledShoot = _.throttle(shoot, 500, { trailing: false });
// Быстрые вызовы
setTimeout(() => throttledShoot(), 0); // Сработает сразу
setTimeout(() => throttledShoot(), 100); // Проигнорирован
setTimeout(() => throttledShoot(), 600); // Сработает (прошло >500мс)// Выстрел! (момент 0мс) // Выстрел! (момент 600мс)
Похожие функции в JavaScript
debounce — другая популярная функция для контроля частоты. Она группирует серию последовательных вызовов и выполняет функцию только один раз после окончания паузы заданной длительности. В отличие от throttle, которая дает гарантированное периодическое выполнение, debounce ждет полной остановки событий.
Использовать throttle предпочтительнее, когда важна регулярность реакции на событие (например, обновление положения элемента при скролле). Debounce больше подходит для финальной обработки, когда действие должно произвестись один раз после серии событий (например, отправка поискового запроса после прекращения ввода).
RequestAnimationFrame (rAF) — альтернатива throttle для анимаций и визуальных обновлений, привязанных к отрисовке кадров браузером (например, при обработке scroll или mousemove для параллакс-эффектов). rAF обеспечивает максимальную плавность, но не контролирует интервал в миллисекундах, а привязывает выполнение к частоте обновления экрана.
Реализации в других языках программирования
Python: В стандартной библиотеки нет точного аналога, но реализуется через декораторы и time.time().
import time
from functools import wraps
def throttle(wait):
def decorator(fn):
last_call = 0
@wraps(fn)
def wrapper(*args, **kwargs):
nonlocal last_call
now = time.time()
if now - last_call >= wait:
last_call = now
return fn(*args, **kwargs)
return wrapper
return decorator
@throttle(1.5)
def my_function():
print("Выполнено", time.time())
for i in range(5):
my_function()
time.sleep(0.5)Выполнено 1633456789.123 Выполнено 1633456790.623 Выполнено 1633456792.123
PHP: Аналогично, реализуется через запоминание времени последнего вызова.
class Throttle {
private $lastExecuted = 0;
private $wait;
public function __construct(float $waitSeconds) {
$this->wait = $waitSeconds;
}
public function run(callable $function, ...$args): ?mixed {
$now = microtime(true);
if (($now - $this->lastExecuted) >= $this->wait) {
$this->lastExecuted = $now;
return $function(...$args);
}
return null;
}
}
$throttler = new Throttle(1.0); // 1 секунда
$result = $throttler->run(function($x) { echo "Call: $x\n"; }, "test");Call: test (при повторном вызове в течение секунды - ничего не выводится)
C#: Часто используется механизм async/await с Task.Delay или библиотека Reactive Extensions (Rx.NET) с оператором Throttle.
Основное отличие от JavaScript реализаций часто заключается в синтаксисе и доступности встроенных средств (например, Rx), но концептуально логика одинакова.
Частые ошибки
1. Потеря контекста (this) при использовании самописной throttle без правильной привязки.
function throttleSimple(func, wait) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= wait) {
lastTime = now;
func(...args); // Потенциальная ошибка: func может потерять свой `this`
}
};
}
const obj = {
value: 1,
logValue() { console.log(this.value); }
};
const throttledLog = throttleSimple(obj.logValue, 1000);
throttledLog(); // Выведет undefined, так как this стал global или undefinedundefined
Решение: Использовать func.apply(this, args) внутри возвращаемой функции.
2. Игнорирование возвращаемого значения оригинальной функции. Дросселированная функция часто возвращает undefined, если вызов был проигнорирован. Это может сломать логику, если ожидается результат.
const throttled = _.throttle((a, b) => a + b, 100);
console.log(throttled(5, 3)); // Может вернуть результат первого вызова
console.log(throttled(2, 1)); // Может вернуть undefined, если интервал не прошел8 undefined
3. Создание множества отдельных throttled-функций для одного и того же исходного действия, что сводит на нет эффект дросселирования.
// ОШИБКА: При каждом рендере создается новая функция
function MyComponent() {
const handleScroll = _.throttle(() => { /* ... */ }, 100);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [handleScroll]); // Зависимость меняется каждый раз
}Решение: Использовать useMemo или useRef для сохранения единственного экземпляра функции.
Эволюция реализации
Сама концепция throttle не претерпевает значительных изменений в стандарте JavaScript, так как это паттерн, а не встроенная функция. Однако популярные библиотеки, такие как Lodash, развивают свои реализации.
В Lodash на протяжении версий:
- Были добавлены опции leading и trailing для тонкого контроля.
- Появились методы cancel и flush у возвращаемой функции для управления запланированными вызовами.
- Улучшена стабильность и обработка крайних случаев (например, когда wait равен 0).
В современных проектах на первый план выходят реализации с использованием React Hooks (например, useThrottle или useThrottledCallback), которые интегрируют логику дросселирования с жизненным циклом компонентов и учитывают особенности функциональных компонентов React.
Расширенные и специальные примеры
Пример 1: Throttle с гарантированным конечным вызовом (trailing) и передачей последних аргументов.
// Эта реализация запоминает контекст и аргументы последнего вызова
function advancedThrottle(func, wait) {
let lastCall = 0;
let timeoutId = null;
let lastContext, lastArgs;
function later() {
if (Date.now() - lastCall >= wait) {
func.apply(lastContext, lastArgs);
timeoutId = lastContext = lastArgs = null;
}
}
return function(...args) {
const now = Date.now();
const remaining = wait - (now - lastCall);
lastContext = this;
lastArgs = args;
if (remaining <= 0 || remaining > wait) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
lastCall = now;
func.apply(this, args);
} else if (!timeoutId) {
timeoutId = setTimeout(later, remaining);
}
};
}
let counter = 0;
const handler = advancedThrottle(function(event) {
console.log(`Счетчик: ${++counter}, X: ${event.clientX}`);
}, 1000);
// Имитация множества быстрых событий mousemove
document.addEventListener('mousemove', handler);
// При активном движении мыши будут trailing-вызовы с последними координатами.Пример 2: Интеграция с Promise и асинхронными функциями.
function throttleAsync(asyncFunc, wait) {
let lastRun = 0;
let pendingPromise = null;
return function(...args) {
const now = Date.now();
if (now - lastRun >= wait) {
lastRun = now;
pendingPromise = asyncFunc(...args);
return pendingPromise;
} else {
return pendingPromise || Promise.reject('Throttled');
}
};
}
const throttledFetch = throttleAsync(fetch, 2000);
// Первый вызов выполнится
throttledFetch('/api/data1').then(r => console.log('1 Done'));
// Второй вызов в течение 2 секунд вернет тот же promise, что и первый
setTimeout(() => {
throttledFetch('/api/data2')
.then(r => console.log('2 Done'))
.catch(e => console.log('2', e)); // 'Throttled'
}, 500);1 Done 2 Throttled
Пример 3: Throttle для последовательности с разными интервалами (ведущий и хвостовой вызовы с разной задержкой).
function dualThrottle(func, { leadingWait, trailingWait }) {
let leadingLastCall = 0;
let trailingTimeout = null;
let lastArgs;
function trailingCall() {
func(...lastArgs);
trailingTimeout = null;
}
return function(...args) {
lastArgs = args;
const now = Date.now();
// Leading edge с одним интервалом
if (now - leadingLastCall >= leadingWait) {
leadingLastCall = now;
func(...args);
}
// Trailing edge с другим интервалом
if (!trailingTimeout) {
trailingTimeout = setTimeout(trailingCall, trailingWait);
}
};
}
// Часто обновляем при начале движения, но делаем финальное обновление реже
const update = dualThrottle(pos => console.log('Update pos:', pos), {
leadingWait: 100,
trailingWait: 500
});
// При потоке событий каждые 50мс:
// Вызовы будут на 0мс (leading), затем на 500мс (trailing), 600мс (leading), 1000мс (trailing) и т.д.