вернуться к уроку

Тормозящий (throttling) декоратор

важность: 5

Создайте «тормозящий» декоратор throttle(f, ms), который возвращает обёртку.

При многократном вызове он передает вызов f не чаще одного раза в ms миллисекунд.

По сравнению с декоратором debounce поведение совершенно другое:

  • debounce запускает функцию один раз после периода «бездействия». Подходит для обработки конечного результата.
  • throttle запускает функцию не чаще, чем указанное время ms. Подходит для регулярных обновлений, которые не должны быть слишком частыми.

Другими словами, throttle похож на секретаря, который принимает телефонные звонки, но при этом беспокоит начальника (вызывает непосредственно f) не чаще, чем один раз в ms миллисекунд.

Давайте рассмотрим реальное применение, чтобы лучше понять это требование и выяснить, откуда оно взято.

Например, мы хотим отслеживать движения мыши.

В браузере мы можем реализовать функцию, которая будет запускаться при каждом перемещении указателя и получать его местоположение. Во время активного использования мыши эта функция запускается очень часто, что-то около 100 раз в секунду (каждые 10 мс). Мы бы хотели обновлять некоторую информацию на странице при передвижении указателя.

…Но функция обновления update() слишком ресурсоёмкая, чтобы делать это при каждом микродвижении. Да и нет смысла делать обновление чаще, чем один раз в 1000 мс.

Поэтому мы обернём вызов в декоратор: будем использовать throttle(update, 1000) как функцию, которая будет запускаться при каждом перемещении указателя вместо оригинальной update(). Декоратор будет вызываться часто, но передавать вызов в update() максимум раз в 1000 мс.

Визуально это будет выглядеть вот так:

  1. Для первого движения указателя декорированный вариант сразу передаёт вызов в update. Это важно, т.к. пользователь сразу видит нашу реакцию на его перемещение.
  2. Затем, когда указатель продолжает движение, в течение 1000 мс ничего не происходит. Декорированный вариант игнорирует вызовы.
  3. По истечению 1000 мс происходит ещё один вызов update с последними координатами.
  4. Затем, наконец, указатель где-то останавливается. Декорированный вариант ждёт, пока не истечёт 1000 мс, и затем вызывает update с последними координатами. В итоге окончательные координаты указателя тоже обработаны.

Пример кода:

function f(a) {
  console.log(a)
}

// f1000 передаёт вызовы f максимум раз в 1000 мс
let f1000 = throttle(f, 1000);

f1000(1); // показывает 1
f1000(2); // (ограничение, 1000 мс ещё нет)
f1000(3); // (ограничение, 1000 мс ещё нет)

// когда 1000 мс истекли ...
// ...выводим 3, промежуточное значение 2 было проигнорировано

P.S. Аргументы и контекст this, переданные в f1000, должны быть переданы в оригинальную f.

Открыть песочницу с тестами для задачи.

function throttle(func, ms) {

  let isThrottled = false,
    savedArgs,
    savedThis;

  function wrapper() {

    if (isThrottled) { // (2)
      savedArgs = arguments;
      savedThis = this;
      return;
    }

    func.apply(this, arguments); // (1)

    isThrottled = true;

    setTimeout(function() {
      isThrottled = false; // (3)
      if (savedArgs) {
        wrapper.apply(savedThis, savedArgs);
        savedArgs = savedThis = null;
      }
    }, ms);
  }

  return wrapper;
}

Вызов throttle(func, ms) возвращает wrapper.

  1. Во время первого вызова обёртка просто вызывает func и устанавливает состояние задержки (isThrottled = true).
  2. В этом состоянии все вызовы запоминаются в saveArgs / saveThis. Обратите внимание, что контекст и аргументы одинаково важны и должны быть запомнены. Они нам нужны для того, чтобы воспроизвести вызов позднее.
  3. Затем по прошествии ms миллисекунд срабатывает setTimeout. Состояние задержки сбрасывается (isThrottled = false). И если мы проигнорировали вызовы, то «обёртка» выполняется с последними запомненными аргументами и контекстом.

На третьем шаге выполняется не func, а wrapper, потому что нам нужно не только выполнить func, но и ещё раз установить состояние задержки и таймаут для его сброса.

Открыть решение с тестами в песочнице.