назад до уроку

Дросельний (throttle) декоратор

важливість: 5

Створіть “дросельний” декоратор throttle(f, ms) – що повертає обгортку.

Коли він викликається кілька разів, він передає виклик до f максимум один раз на ms мілісекунд.

Різниця з debounce полягає в тому, що це зовсім інший декоратор:

  • debounce запускає функцію один раз після періоду “спокою”. Це добре для обробки кінцевого результату.
  • throttle запускає функцію не частіше, ніж дано ms часу. Це добре для регулярних оновлень, які не повинні бути дуже часто.

Іншими словами, throttle – це як секретар, який приймає телефонні дзвінки, але турбує боса (викликає фактичну f) не частіше, ніж один раз на ms мілісекунд.

Перевірмо застосунок з реального життя, щоб краще зрозуміти цю вимогу та побачити, звідки вона походить.

Наприклад, ми хочемо відстежувати рухи миші.

У браузері ми можемо налаштувати запускати функцію при кожному русі миші та отримати місце курсору та те, як він рухається. Під час активного використання миші ця функція зазвичай працює дуже часто, може бути щось на зразок 100 разів на секунду (кожні 10 мс). Ми хотіли б оновити деяку інформацію на вебсторінці, коли курсор рухається.

…Але функція оновлення update() занадто тяжка, щоб виконувати її після кожного найменшого руху миші. Та й нам немає сенсу в оновленні частіше, ніж один раз на 100 мс.

Отже, ми загорнемо її в декоратор: використаємо throttle(update, 100) як функцію для запуску після кожного руху миші замість оригінального update(). Сам декоратор буде викликатися часто, але передаватиме виклик до update() максимум один раз на 100 мс.

Візуально, це буде виглядати так:

  1. Для першого руху миші декорований варіант негайно передає виклик до update. Це важливо, щоб користувач негайно побачив нашу реакцію на його рух.
  2. Після того, як миша рухається, до 100ms нічого не відбувається. Декорований варіант ігнорує виклики.
  3. Наприкінці 100ms – ще один update відбувається з останніми координатами.
  4. Тоді, нарешті, миша зупиняється десь. Декорований варіант чекає, доки мине 100ms, а потім запускає 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 ms time out ...
// ...виводить 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;
    }
    isThrottled = true;

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

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

  return wrapper;
}

Виклик throttle(func, ms) повертає wrapper.

  1. Під час першого виклику wrapper просто викликає func і встановлює стан відпочинку (isThrottled = true).
  2. Під час цього стану всі виклики запам’ятовуються в savedArgs/savedThis. Зверніть увагу, що як контекст, так і аргументи однаково важливі, і повинні бути запам’ятованими. Для кожного повноцінного виклику, нам потрібні і аргумент, і контекст одночасно.
  3. Після того, як минає ms мілісекунд, setTimeout спрацьовує. Стан відпочинку знімається (isThrottled = false) і, якщо ми мали проігноровані виклики, wrapper виконується з останніми збереженими аргументами та контекстом.

3-й крок запускає не func, а wrapper, тому що ми не тільки повинні виконувати func, але й ще раз вводити стан відпочинку та налаштовувати тайм-аут, щоб потім знову відмінити стан відпочинку.

Відкрити рішення із тестами в пісочниці.