← Назад к вопросам

Реализовать функцию throttle

1.7 Middle🔥 241 комментариев
#JavaScript Core

Условие

Напишите функцию throttle(fn, interval), которая ограничивает частоту вызова переданной функции fn - не чаще, чем раз в interval миллисекунд.

Требования

  1. Функция должна принимать два аргумента:

    • fn - функция, вызов которой нужно ограничить
    • interval - минимальный интервал между вызовами в миллисекундах
  2. Возвращаемая функция должна:

    • Вызываться сразу при первом обращении
    • Игнорировать вызовы в течение интервала
    • Корректно передавать аргументы в оригинальную функцию

Пример использования

var throttledScroll = throttle(handleScroll, 1000);

function handleScroll() {
  console.log(Date.now());
}

window.addEventListener("scroll", throttledScroll);

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение: Реализация функции throttle

Различие между throttle и debounce

В отличие от debounce, который откладывает вызов до конца периода спокойствия, throttle выполняет функцию максимум один раз за заданный интервал времени. Это критично при обработке часто возникающих событий типа scroll или mousemove, где нужна регулярная обработка без перегрузки.

Базовая реализация

function throttle(fn, interval) {
  let lastCallTime = 0;
  
  return function throttled(...args) {
    const now = Date.now();
    
    if (now - lastCallTime >= interval) {
      lastCallTime = now;
      fn.apply(this, args);
    }
  };
}

Версия с трейлингим вызовом

function throttle(fn, interval) {
  let lastCallTime = 0;
  let timeoutId = null;
  
  const throttled = function(...args) {
    const now = Date.now();
    
    if (now - lastCallTime >= interval) {
      // Очищаем таймер если был
      if (timeoutId !== null) {
        clearTimeout(timeoutId);
        timeoutId = null;
      }
      
      lastCallTime = now;
      fn.apply(this, args);
    } else if (timeoutId === null) {
      // Планируем вызов в конце интервала
      const remainingTime = interval - (now - lastCallTime);
      timeoutId = setTimeout(() => {
        lastCallTime = Date.now();
        fn.apply(this, args);
        timeoutId = null;
      }, remainingTime);
    }
  };
  
  throttled.cancel = () => {
    if (timeoutId !== null) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }
  };
  
  return throttled;
}

TypeScript версия

type ThrottledFn<T extends (...args: any[]) => any> = (
  ...args: Parameters<T>
) => void & { cancel: () => void };

function throttle<T extends (...args: any[]) => any>(
  fn: T,
  interval: number
): ThrottledFn<T> {
  let lastCallTime = 0;
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
  
  const throttled = function(this: any, ...args: Parameters<T>) {
    const now = Date.now();
    const timeSinceLastCall = now - lastCallTime;
    
    if (timeSinceLastCall >= interval) {
      if (timeoutId !== null) {
        clearTimeout(timeoutId);
        timeoutId = null;
      }
      
      lastCallTime = now;
      fn.apply(this, args);
    } else if (timeoutId === null) {
      const remainingTime = interval - timeSinceLastCall;
      timeoutId = setTimeout(() => {
        lastCallTime = Date.now();
        fn.apply(this, args);
        timeoutId = null;
      }, remainingTime);
    }
  };
  
  throttled.cancel = () => {
    if (timeoutId !== null) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }
  };
  
  return throttled as ThrottledFn<T>;
}

Варианты с опциями

function throttle(fn, interval, options = {}) {
  const { leading = true, trailing = true } = options;
  
  let lastCallTime = leading ? 0 : Date.now();
  let timeoutId = null;
  
  return function throttled(...args) {
    const now = Date.now();
    
    if (!leading && lastCallTime === 0) {
      lastCallTime = now;
    }
    
    const timeSinceLastCall = now - lastCallTime;
    
    if (timeSinceLastCall >= interval) {
      if (timeoutId !== null) {
        clearTimeout(timeoutId);
        timeoutId = null;
      }
      
      lastCallTime = now;
      fn.apply(this, args);
    } else if (trailing && timeoutId === null) {
      const remainingTime = interval - timeSinceLastCall;
      timeoutId = setTimeout(() => {
        if (trailing) {
          fn.apply(this, args);
        }
        timeoutId = null;
      }, remainingTime);
    }
  };
}

// leading: true - вызывает сразу при первом обращении
// trailing: true - вызывает в конце интервала если было несколько вызовов

Примеры использования

// Обработка скролла
function handleScroll() {
  console.log('Scroll event at', Date.now());
}

const throttledScroll = throttle(handleScroll, 1000);
window.addEventListener('scroll', throttledScroll);

// Обработка изменения размера окна
function handleResize() {
  console.log('Window resized');
}

const throttledResize = throttle(handleResize, 500);
window.addEventListener('resize', throttledResize);

// Движение мыши
function handleMouseMove(e) {
  console.log(`Mouse: ${e.clientX}, ${e.clientY}`);
}

const throttledMove = throttle(handleMouseMove, 100);
document.addEventListener('mousemove', throttledMove);

// Авто-сохранение
const saveData = throttle(() => {
  console.log('Saving...');
  fetch('/api/save', { method: 'POST' });
}, 5000);

input.addEventListener('input', saveData);

// Отмена
const fn = throttle(() => console.log('Executed'), 1000);
fn();
fn.cancel(); // Не выполнится отложенный вызов

Визуализация throttle vs debounce

Вызовы:       |----|----|----|----|----|----|----|

Throttle      |####|    |####|    |####|    |####|
(каждую секунду)

Debounce      |                            |####|
(после паузы)

Территория: 4 вызова, интервал 1 сек

Пошаговый разбор

const fn = throttle(() => console.log(Date.now()), 1000);

fn(); // t=0ms   → вызывает сразу
fn(); // t=100ms → игнорирует
fn(); // t=500ms → игнорирует
fn(); // t=1500ms → вызывает (прошло 1500мс)
fn(); // t=1600ms → игнорирует
fn(); // t=2000ms → вызывает (прошло 500мс с последнего)

Сравнение подходов

ПодходLeadingTrailingПлюсыМинусы
ПростойtruefalseЛегко реализоватьТеряет последние данные
С trailingtruetrueПолнота данныхСложнее
С опциямилюбойлюбойГибкостьМного кода

Реальные применения

  • Scroll события: обновление UI при прокрутке
  • Resize события: пересчёт layout при изменении размера
  • Search input: дебаунсинг API запросов (часто комбинируют)
  • Mouse move: отслеживание курсора без перегрузки
  • Analytics: отправка событий регулярно

Ключевые моменты

  • lastCallTime: отслеживаем время последнего вызова
  • Date.now(): высокоточное время вызова
  • timeSinceLastCall: проверяем прошёл ли интервал
  • Trailing option: вызов в конце интервала если надо
  • Cancel метод: возможность отменить отложенный вызов