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

Дросельний (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, але й ще раз вводити стан відпочинку та налаштовувати тайм-аут, щоб скинути його.

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