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