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 — ключевое отличие для собеседования.