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