Методы замедления (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 в этом году.

Комментарии (1)


  1. Alexandroppolus
    14.06.2024 08:08
    +2

    Насколько я знаю, throttle при невозможности вызвать функцию "прямо сейчас", запоминает последние переданные аргументы и this, чтобы сделать вызов, когда закончится ожидание. Так что без setTimeout не обойтись.

    Ну и раз уж примеры на TS, то и типизацию надо нормальную добавить, не any[]