Тормозящий (throttling) декоратор
Создайте «тормозящий» декоратор throttle(f, ms)
, который возвращает обёртку.
При многократном вызове он передает вызов f
не чаще одного раза в ms
миллисекунд.
По сравнению с декоратором debounce
поведение совершенно другое:
debounce
запускает функцию один раз после периода «бездействия». Подходит для обработки конечного результата.throttle
запускает функцию не чаще, чем указанное времяms
. Подходит для регулярных обновлений, которые не должны быть слишком частыми.
Другими словами, throttle
похож на секретаря, который принимает телефонные звонки, но при этом беспокоит начальника (вызывает непосредственно f
) не чаще, чем один раз в ms
миллисекунд.
Давайте рассмотрим реальное применение, чтобы лучше понять это требование и выяснить, откуда оно взято.
Например, мы хотим отслеживать движения мыши.
В браузере мы можем реализовать функцию, которая будет запускаться при каждом перемещении указателя и получать его местоположение. Во время активного использования мыши эта функция запускается очень часто, что-то около 100 раз в секунду (каждые 10 мс). Мы бы хотели обновлять некоторую информацию на странице при передвижении указателя.
…Но функция обновления update()
слишком ресурсоёмкая, чтобы делать это при каждом микродвижении. Да и нет смысла делать обновление чаще, чем один раз в 1000 мс.
Поэтому мы обернём вызов в декоратор: будем использовать throttle(update, 1000)
как функцию, которая будет запускаться при каждом перемещении указателя вместо оригинальной update()
. Декоратор будет вызываться часто, но передавать вызов в update()
максимум раз в 1000 мс.
Визуально это будет выглядеть вот так:
- Для первого движения указателя декорированный вариант сразу передаёт вызов в
update
. Это важно, т.к. пользователь сразу видит нашу реакцию на его перемещение. - Затем, когда указатель продолжает движение, в течение 1000 мс ничего не происходит. Декорированный вариант игнорирует вызовы.
- По истечению 1000 мс происходит ещё один вызов
update
с последними координатами. - Затем, наконец, указатель где-то останавливается. Декорированный вариант ждёт, пока не истечёт 1000 мс, и затем вызывает
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 мс истекли ...
// ...выводим 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;
}
func.apply(this, arguments); // (1)
isThrottled = true;
setTimeout(function() {
isThrottled = false; // (3)
if (savedArgs) {
wrapper.apply(savedThis, savedArgs);
savedArgs = savedThis = null;
}
}, ms);
}
return wrapper;
}
Вызов throttle(func, ms)
возвращает wrapper
.
- Во время первого вызова обёртка просто вызывает
func
и устанавливает состояние задержки (isThrottled = true
). - В этом состоянии все вызовы запоминаются в
saveArgs / saveThis
. Обратите внимание, что контекст и аргументы одинаково важны и должны быть запомнены. Они нам нужны для того, чтобы воспроизвести вызов позднее. - Затем по прошествии
ms
миллисекунд срабатываетsetTimeout
. Состояние задержки сбрасывается (isThrottled = false
). И если мы проигнорировали вызовы, то «обёртка» выполняется с последними запомненными аргументами и контекстом.
На третьем шаге выполняется не func
, а wrapper
, потому что нам нужно не только выполнить func
, но и ещё раз установить состояние задержки и таймаут для его сброса.