Перейти к содержимому

Throttle

Throttle ограничивает частоту вызовов до максимум одного раза за интервал. На собеседованиях просят реализовать с нуля и объяснить разницу с debounce: throttle гарантирует регулярные вызовы при непрерывном потоке событий (scroll, mousemove), debounce — паузу после серии. Интервьюер ждёт опции leading/trailing.

Полная timestamp-реализация с leading/trailing (interview-grade):

/**
* Гарантирует, что fn вызывается не чаще одного раза за limit мс.
* @param {Function} fn
* @param {number} limit
* @param {{ leading?: boolean; trailing?: boolean }} [opts]
*/
function throttle(fn, limit, { leading = true, trailing = true } = {}) {
let lastCallTime = 0; // время последнего реального вызова fn
let timerId;
let lastCtx, lastArgs;
function invoke(time) {
lastCallTime = time;
fn.apply(lastCtx, lastArgs);
}
function throttled(...args) {
const now = Date.now();
lastCtx = this;
lastArgs = args;
// Пропустить leading: сделать вид что вызов уже был (но без invoke)
if (!lastCallTime && !leading) lastCallTime = now;
const remaining = limit - (now - lastCallTime);
if (remaining <= 0 || remaining > limit) {
// Пора вызывать
if (timerId) { clearTimeout(timerId); timerId = undefined; }
invoke(now);
} else if (!timerId && trailing) {
// Запланировать trailing-вызов
timerId = setTimeout(() => {
timerId = undefined;
lastCallTime = leading ? Date.now() : 0;
fn.apply(lastCtx, lastArgs);
}, remaining);
}
}
throttled.cancel = () => {
clearTimeout(timerId);
timerId = undefined;
lastCallTime = 0;
};
return throttled;
}
// usage: прогресс-бар прокрутки — не чаще 1 раза / 60 мс
const onScroll = throttle(() => updateProgressBar(), 60);
window.addEventListener('scroll', onScroll);
// Сравнение:
// debounce(fn, 300) при непрерывном скролле = вызов ТОЛЬКО после остановки
// throttle(fn, 300) при непрерывном скролле = вызов каждые ~300 мс

Простые версии — показываем как разминку перед полной реализацией:

// ─── Timestamp-версия (leading only) — простейший вариант ────────────────
function throttleTimestamp(fn, limit) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= limit) {
lastTime = now;
return fn.apply(this, args);
}
};
}
// ─── Flag-версия (leading only, без точного времени) ──────────────────────
function throttleFlag(fn, limit) {
let waiting = false;
return function (...args) {
if (!waiting) {
fn.apply(this, args);
waiting = true;
setTimeout(() => { waiting = false; }, limit);
}
};
}
// Когда throttle, когда debounce?
// ─ scroll / mousemove / resize → throttle (нужен прогресс в реальном времени)
// ─ search input / form autosave → debounce (нужен финальный результат после паузы)
// ─ защита от двойного клика → throttle { leading:true, trailing:false }
// ─ window resize → пересчёт → debounce (нас интересует конечный размер)
// Различие timestamp vs flag:
// timestamp: учитывает реальное прошедшее время → точнее
// flag: ровно limit мс после первого вызова → проще, чуть менее точно

Сложность

  • Время: O(1) на каждый вызов
  • Память: O(1) — один таймер и timestamp последнего вызова

Итог: Throttle через timestamp: leading/trailing опции, cancel(). Timestamp-версия точнее flag-версии. Где throttle, где debounce — ключевое отличие для собеседования.