Дросельний (throttle) декоратор
Створіть “дросельний” декоратор throttle(f, ms)
– що повертає обгортку.
Коли він викликається кілька разів, він передає виклик до f
максимум один раз на ms
мілісекунд.
Різниця з debounce полягає в тому, що це зовсім інший декоратор:
debounce
запускає функцію один раз після періоду “спокою”. Це добре для обробки кінцевого результату.throttle
запускає функцію не частіше, ніж даноms
часу. Це добре для регулярних оновлень, які не повинні бути дуже часто.
Іншими словами, throttle
– це як секретар, який приймає телефонні дзвінки, але турбує боса (викликає фактичну f
) не частіше, ніж один раз на ms
мілісекунд.
Перевірмо застосунок з реального життя, щоб краще зрозуміти ці вимогу та побачити, звідки вони походять.
Наприклад, ми хочемо відстежувати рухи миші.
У браузері ми можемо налаштувати запускати функцію при кожному русі миші та отримати місце курсору та те, як він рухається. Під час активного використання миші ця функція зазвичай працює дуже часто, може бути щось на зразок 100 разів на секунду (кожні 10 мс). Ми хотіли б оновити деяку інформацію на вебсторінці, коли курсор рухається.
…Але функція оновлення update()
занадто тяжка, щоб виконувати її це на кожному мікрорусі. Також немає сенсу в оновленні частіше, ніж один раз на 100 мс.
Отже, ми загорнемо її в декоратор: використовуємо throttle(update, 100)
як функцію для запуску на кожному переміщенні миші замість оригінального update()
. Декоратор буде викликатися часто, але передавали виклик до update()
максимум один раз на 100 мс.
Візуально, це буде виглядати так:
- Для першого руху миші декорований варіант негайно передає виклик до
update
. Це важливо, що користувач негайно побачить нашу реакцію на його рух. - Після того, як миша рухається, до
100ms
нічого не відбувається. Декорований варіант ігнорує виклики. - Наприкінці
100ms
– ще одинupdate
відбувається з останніми координатами. - Тоді, нарешті, миша зупиняється десь. Декорований варіант чекає, доки
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
.
- Під час першого виклику
wrapper
просто викликаєfunc
і встановлює стан відпочинку (isThrottled = true
). - У цьому стані всі виклики запам’ятовуються в
savedArgs/savedThis
. Зверніть увагу, що як контекст, так і аргументи однаково важливі, і повинні бути запам’ятованими. Нам потрібні вони їх одночасно, щоб відтворити виклик. - Після того, як
ms
мілісекунди проходять,setTimeout
спрацьовує. Стан відпочинку знімається (isThrottled = false
) і, якщо ми мали проігноровані виклики,wrapper
виконується з останніми запам’ятовуваними аргументами та контекстом.
3-й крок запускає не func
, а wrapper
, тому що ми не тільки повинні виконувати func
, але й ще раз вводити стан відпочинку та налаштовувати тайм-аут, щоб скинути його.