Методы замедления (Throttling) служат для контроля того, сколько раз мы разрешаем выполнение функции за определенный период времени. Обычно throttling реализуется через Higher Order Function. Функция - обертка должна контролировать, чтобы callback функция вызывалась не чаще одного раза каждые X миллисекунд. Callback функция вызывается немедленно и не может быть вызвана снова в течение оставшегося времени ожидания.
Задача на реализацию Throttling часто дается на интервью и на первый взгляд кажется тривиальной, но и тут есть свои нюансы.
Давайте реализуем функцию throttle, которая принимает функцию callback и время ожидания. Вызов throttle() должен возвращать новую функцию, которая будет вызывать внутри себя callback функцию в соответствии с описанным выше поведением.
Примеры использования
let i = 0;
function increment() {
i++;
}
const throttledIncrement = throttle(increment, 100);
// t = 0: Call throttledIncrement(). i is now 1.
throttledIncrement(); // i = 1
// t = 50: Call throttledIncrement() again.
// i is still 1 because 100ms have not passed.
throttledIncrement(); // i = 1
// t = 101: Call throttledIncrement() again. i is now 2.
// i can be incremented because it has been more than 100ms
// since the last throttledIncrement() call at t = 0.
throttledIncrement(); // i = 2
Решение
Существует два основных способа решения данной задачи - с использованием setTimeout или с использованием Date.now()
Решение через Date.now()
/**
* @callback func
* @param {number} wait
* @return {Function}
*/
export default function throttle(func, wait) {
let lastCallTime = null;
return function (...args: any[]) {
const now = Date.now();
const passed = now - lastCallTime;
if(passed > wait){
func.apply(this, args);
lastCallTime = Date.now();
}
}
}
Рассмотрим решение через Date.now(). Решение довольно тривиальное - мы запоминаем последнее время вызова, и используем его для определения, нужно ли вызывать функцию func, или нет. На мой взгляд - данное решение предпочтительнее, поскольку оно не добавляет новый вызов в Event Loop.
Решение через setTimeout()
/**
* @callback func
* @param {number} wait
* @return {Function}
*/
export default function throttle(func, wait) {
let shouldThrottle = false
return function (...args: any[]) {
if(!shouldThrottle){
func.apply(this, args);
shouldThrottle = true;
setTimeout(() => {
shouldThrottle = false;
}, wait);
}
}
}
Тут тоже все довольно тривиально - мы используем setTimeout для выставления актуального значения флага shouldThrottle. На основании текущего значения флага, мы определяем, нужно или нет вызывать функцию func.
На что стоит обратить внимание?
Будьте внимательны с тем, чтобы функция корректно работала с параметром this.
Вызов исходной функции func должен сохранять исходный указатель this. Поэтому:
Arrow-функции нельзя использовать для объявления внутренней функции.
Вызов исходной функции через func(...args) также нельзя использовать.
Для вызова исходной функции, предпочтительнее использовать func.apply или func.call, в зависимости от Ваших предпочтений (с появлением spread оператора они по большей части эквивалентны).
В следующей статье, я хочу разобрать решение задачи с Leetcode 2636, Promise Pool. Похожую (немного усложненную) задачу давали в Yandex на Two Days Offer в этом году.
Alexandroppolus
Насколько я знаю, throttle при невозможности вызвать функцию "прямо сейчас", запоминает последние переданные аргументы и this, чтобы сделать вызов, когда закончится ожидание. Так что без setTimeout не обойтись.
Ну и раз уж примеры на TS, то и типизацию надо нормальную добавить, не any[]