Троттлинг и защита от тряски

JavaScript

Debounce и Throttle — две концепции, которые мы используем в JavaScript для усиления контроля над выполнением функций, что особенно полезно в обработчиках событий. Оба метода отвечают на один и тот же вопрос: «Как часто функция вызывается в течение определенного периода времени?»

📚 Ссылки по теме

Большая часть контента в статье взята из следующих статей, вторгайтесь и удаляйте!

📚 Отказаться

1. Концепция

  • Это концепция «устранения дребезга» механических переключателей. После нажатия пружинного переключателя из-за действия язычка точка контакта будет постоянно контактировать и разъединяться много раз. Вплоть до стабильного периода времени без власть

  • Во фронтенд-разработке запускаются некоторые частые события

    • мышь(mousemove...)клавиатура(keydown...) события и т. д.
    • Проверка форм в режиме реального времени (частая отправка запросов на проверку)
  • Функция обратного вызова выполняется после задержки в миллисекундах, если функция устранения дребезга не вызывается снова, например

    • существуетmousemoveВ этом случае убедитесь, что несколько триггеров вызывают функцию прослушивателя только один раз.
    • Когда форма проверена, защита от сотрясений не добавляется, и ввод осуществляется последовательно.user, будет делиться наu,us,use,userОтправьте запрос четыре раза и добавьте защиту от сотрясений, установите время, вы можете добиться полного вводаuserпроверить запрос

2. Идеи

  • Из функции debounce видно, что функция анти-шейка получает как минимум два параметра (3 параметра в популярных библиотеках классов)

    • Перезвонитеfn
    • время задержкиdelay
  • функция устранения дребезгавозвращать закрытие, закрытие часто вызывается

    • Функция debounce вызывается только один раз, а возвращаемая ею функция закрытия вызывается после этого.
    • Функция обратного вызова ограничена закрытиемfnвыполнение, обеспечивает только одно выполнение после остановки непрерывной работы
  • Замыкание используется для того, чтобы переменная, указывающая на таймер, неgcПерерабатывать

    • реализовано во времени задержкиdelayНепрерывный триггер внутри не выполняет функцию обратного вызоваfn, используя таймер, установленный внутри замыканияsetTimeOut
    • Часто вызывайте это закрытие и очищайте последний вызванный таймер при каждом вызове.
    • Переменная, сохраненная замыканием, указывает на последний установленный таймер.

3. Реализация

  • Простая реализация по принципу

    function debounce(fn, delay) {
      var timer;
      return function() {
        // 清除上一次调用时设置的定时器
        // 计时器清零
        clearTimeout(timer);
        // 重新设置计时器
        timer = setTimeout(fn, delay);
      };
    }
    
  • Простая реализация кода может вызвать две проблемы

    • thisуказать на проблему. Функция debounce вызывает функцию обратного вызова в таймере.fn,такfnпри исполненииthisуказывает на глобальный объект (в браузереwindow), вам нужно использовать переменную во внешнем слое, чтобыthisсохранить его, использоватьapplyсделать явную привязку

      function debounce(fn, delay) {
        var timer;
        return function() {
          // 保存调用时的this
          var context = this;
          clearTimeout(timer);
          timer = setTimeout(function() {
            // 修正 this 的指向
            fn.apply(this);
          }, delay);
        };
      }
      
    • eventобъект. Объекты событий предоставляются в обработчиках событий JavaScript.event, вам нужно передать этот объект события при вызове закрытия

      function debounce(fn, delay) {
        var timer;
        return function() {
          // 保存调用时的this
          var context = this;
          // 保存参数
          var args = arguments;
          clearTimeout(timer);
          timer = setTimeout(function() {
            console.log(context);
            // 修正this,并传入参数
            fn.apply(context, args);
          }, delay);
        };
      }
      

4. Идеально (underscoreреализация)

  • Выполнить немедленно. Добавить третий параметр, два случая

    • Сначала выполните функцию обратного вызоваfn, подождите, пока после прекращения срабатыванияdelayмиллисекунд перед повторным срабатыванием (выполнить первым)
    • Непрерывный вызов функции debounce не запускает функцию обратного вызова и останавливает вызов послеdelayФункция обратного вызова выполняется через миллисекунды (после казни)
    • clearTimeout(timer)назад,timerне становитсяnull, но по-прежнему указывает на объект таймера
    function debounce(fn, delay, immediate) {
      var timer;
      return function() {
        var context = this;
        var args = arguments;
        // 停止定时器
        if (timer) clearTimeout(timer);
        // 回调函数执行的时机
        if (immediate) {
          // 是否已经执行过
          // 执行过,则timer指向定时器对象,callNow 为 false
          // 未执行,则timer 为 null,callNow 为 true
          var callNow = !timer;
          // 设置延时
          timer = setTimeout(function() {
            timer = null;
          }, delay);
          if (callNow) fn.apply(context, args);
        } else {
          // 停止调用后delay时间才执行回调函数
          timer = setTimeout(function() {
            fn.apply(context, args);
          }, delay);
        }
      };
    }
    
  • Возвращаемое значение и отмена функции устранения дребезга

    • Функции обратного вызова могут иметь возвращаемые значения.
      • В ситуации после выполнения не требуется учитывать возвращаемое значение, поскольку в течение периода до выполнения функции обратного вызова возвращаемое значение всегдаundefined
      • Выполните сначала, вы получите возвращаемое значение первым
    • Можно отменить функцию устранения дребезга. обычно, когдаimmediateдляtrueКогда он срабатывает один раз, вы должны ждать егоdelayЕго можно запустить снова через некоторое время, но если вы хотите снова запустить в течение этого периода времени, вы можете сначала отменить предыдущую функцию устранения дребезга.
    function debounce(fn, delay, immediate) {
      var timer, result;
      var debounced = function() {
        var context = this;
        var args = arguments;
        // 停止定时器
        if (timer) clearTimeout(timer);
        // 回调函数执行的时机
        if (immediate) {
          // 是否已经执行过
          // 执行过,则timer指向定时器对象,callNow 为 false
          // 未执行,则timer 为 null,callNow 为 true
          var callNow = !timer;
          // 设置延时
          timer = setTimeout(function() {
            timer = null;
          }, delay);
          if (callNow) result = fn.apply(context, args);
        } else {
          // 停止调用后delay时间才执行回调函数
          timer = setTimeout(function() {
            fn.apply(context, args);
          }, delay);
        }
        // 返回回调函数的返回值
        return result;
      };
    
      // 取消操作
      debounced.cancel = function() {
        clearTimeout(timer);
        timer = null;
      };
    
      return debounced;
    }
    
  • нотация ES6

    function debounce(fn, delay, immediate) {
      let timer, result;
      // 这里不能使用箭头函数,不然 this 依然会指向 Windows对象
      // 使用rest参数,获取函数的多余参数
      const debounced = function(...args) {
        if (timer) clearTimeout(timer);
        if (immediate) {
          const callNow = !timer;
          timer = setTimeout(() => {
            timer = null;
          }, delay);
          if (callNow) result = fn.apply(this, args);
        } else {
          timer = setTimeout(() => {
            fn.apply(this, args);
          }, delay);
        }
        return result;
      };
    
      debounced.cancel = () => {
        clearTimeout(timer);
        timer = null;
      };
    
      return debounced;
    }
    

📚 дроссель

1. Концепция

  • фиксированная скорость выполнения функции

  • Если событие продолжает запускаться, выполняйте его время от времени.

    • например мониторингmousemoveСобытие, независимо от скорости движения мыши, после [дроссельной заслонки] функция слушателя будет выполнена в течение нескольких секунд, подождите максимально один раз, и, таким образом, вызвать выполнение [Универса]
  • windowизresize,scrollОптимизация событий и т. д.

2. Идеи

  • Есть две основные реализации

    • использовать отметку времени
    • установить таймер
  • После того, как функция дроссельной заслонки вызывается дроссельной заслонкойвозвращать закрытие

    • Замыкания используются для хранения предыдущих временных меток или переменных таймера (потому чтоНа переменную ссылается возвращаемая функция, поэтому она не может быть переработана механизмом сборки мусора.)
  • Метод метки времени

    • Когда событие инициируется, возьмите текущую временную метку и вычтите предыдущую временную метку (изначально установлено значение 0).
    • Если результат больше установленного периода времени, выполните функцию, а затем обновите временную метку до текущей временной метки.
    • Если результат меньше установленного периода времени, функция не будет выполнена
  • режим таймера

    • Когда событие срабатывает, установите таймер
    • Когда событие запускается снова, если таймер существует, оно не будет выполняться до тех пор, пока таймер не будет выполнен, а затем выполняется функция для очистки таймера.
    • установить следующий таймер
  • Комбинация этих двух методов может привести к тому, что слияние будет выполнено немедленно и все еще выполняется один раз после остановки триггера.

3. Реализация

  • Реализация метки времени

    function throttle(fn, wait) {
      var args;
      // 前一次执行的时间戳
      var previous = 0;
      return function() {
        // 将时间转为时间戳
        var now = +new Date();
        args = arguments;
        // 时间间隔大于延迟时间才执行
        if (now - previous > wait) {
          fn.apply(this, args);
          previous = now;
        }
      };
    }
    
    • Запустите событие прослушивания, функция обратного вызова будет выполнена немедленно (начальныйpreviousравен 0, если установленный временной интервал не превышает отметку времени текущего времени, разница должна быть больше временного интервала)
    • После остановки триггера, где бы ни было время остановки, он больше не будет выполняться. Например, выполнить 1 раз в 1 секунду и остановиться на 4,2 секунды, тогда не будет выполняться 1 раз в 5 секунд
  • Реализация таймера

    function throttle(fn, wait) {
      var timer, context, args;
      return function() {
        context = this;
        args = arguments;
        // 如果定时器存在,则不执行
        if (!timer) {
          timer = setTimeout(function() {
            // 执行后释放定时器变量
            timer = null;
            fn.apply(context, args);
          }, wait);
        }
      };
    }
    
    • Функция обратного вызова не будет выполняться немедленно. Она будет выполнена в первый раз через секунды ожидания. После остановки закрытия, если время остановки находится между двумя выполнениями, она будет выполнена снова.
  • Сочетание метки времени и реализации таймера

    function throttle(fn, wait) {
      var timer, context, args;
      var previous = 0;
      // 延时执行函数
      var later = function() {
        previous = +new Date();
        // 执行后释放定时器变量
        timer = null;
        fn.apply(context, args);
        if (!timeout) context = args = null;
      };
      var throttled = function() {
        var now = +new Date();
        // 距离下次执行 fn 的时间
        // 如果人为修改系统时间,可能出现 now 小于 previous 情况
        // 则剩余时间可能超过时间周期 wait
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        // 没有剩余时间 || 修改系统时间导致时间异常,则会立即执行回调函数fn
        // 初次调用时,previous为0,除非wait大于当前时间的时间戳,否则剩余时间一定小于0
        if (remaining <= 0 || remaining > wait) {
          // 如果存在延时执行定时器,将其取消掉
          if (timer) {
            clearTimeout(timer);
            timer = null;
          }
          previous = now;
          fn.apply(context, args);
          if (!timeout) context = args = null;
        } else if (!timer) {
          // 设置延时执行
          timer = setTimeout(later, remaining);
        }
      };
      return throttled;
    }
    
    • Функция дросселирования в процессе реализуется по принципу метки времени, одновременно реализуется и немедленное исполнение.
    • Таймер просто используется для добавления задержки для выполнения в конце выхода.
    • Таймер будет переустанавливать время каждый раз, когда он срабатывает, но пока он не перестанет срабатывать, функция обратного вызова fn не будет выполняться.

4. Оптимизация и улучшение

  • Добавьте третий параметр, чтобы пользователи могли сами выбирать режим.

    • Игнорировать вызовы на стартовых границах, пройти{ leading: false }
    • Игнорировать вызовы на замыкающих границах, передать{ trailing: false }
  • Добавить функцию возвращаемого значения

  • Добавить функцию отмены

    function throttle(func, wait, options) {
      var context, args, result;
      var timeout = null;
      // 上次执行时间点
      var previous = 0;
      if (!options) options = {};
      // 延迟执行函数
      var later = function() {
        // 若设定了开始边界不执行选项,上次执行时间始终为0
        previous = options.leading === false ? 0 : new Date().getTime();
        timeout = null;
        // func 可能会修改 timeout 变量
        result = func.apply(context, args);
        // 定时器变量引用为空,表示最后一次执行,则要清除闭包引用的变量
        if (!timeout) context = args = null;
      };
      var throttled = function() {
        var now = new Date().getTime();
        // 首次执行时,如果设定了开始边界不执行选项,将上次执行时间设定为当前时间。
        if (!previous && options.leading === false) previous = now;
        // 延迟执行时间间隔
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        // 延迟时间间隔remaining小于等于0,表示上次执行至此所间隔时间已经超过一个时间窗口
        // remaining 大于时间窗口 wait,表示客户端系统时间被调整过
        if (remaining <= 0 || remaining > wait) {
          if (timeout) {
            clearTimeout(timeout);
            timeout = null;
          }
          previous = now;
          result = func.apply(context, args);
          if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
          timeout = setTimeout(later, remaining);
        }
        // 返回回调函数执行后的返回值
        return result;
      };
      throttled.cancel = function() {
        clearTimeout(timeout);
        previous = 0;
        timeout = context = args = null;
      };
      return throttled;
    }
    
    • Существует проблема,leading: falseа такжеtrailing: falseнельзя установить одновременно
      • Первая граница запуска не выполняется, однако при первом срабатыванииpreviousравно 0, тогдаremainingценность иwaitравный. так,if (!previous && options.leading === false)правда, поменялpreviousзначение, в то время какif (remaining <= 0 || remaining > wait)ложный
      • Запуск позже приведет кif (!previous && options.leading === false)ложно, в то время какif (remaining <= 0 || remaining > wait)истинный. Это становится началом граничного исполнения. так что сleading: falseконфликт

📚 Резюме

  • На сегодняшний день полная реализацияunderscoreФункции подавления дребезга и газа в
  • а такжеlodashРеализация функции подавления дребезга и дроссельной заслонки более сложная, а инкапсуляция более тщательная.
  • Для визуализации процесса выполнения рекомендуются два инструмента.
  • Цель самостоятельной реализации - изучить идеи.В реальной разработке попробуйте использовать библиотеки классов, такие как lodash или underscore.

В сравнении

  • Throttle и debounce — два решения проблемы несоответствия между скоростями запросов и ответов. Разница между ними заключается в выборе разных стратегий.

  • Феномен тайм-аута лифта объясняет разницу между ними. Предполагая, что лифт настроен на 15 секунд, независимо от ограничений пропускной способности.

    • throttleСтратегия: сделать так, чтобы если лифт после того, как первые люди вошли вовремя, время доставки через 15 секунд, не ждать. Если никто не ждет,
    • debounceСтратегия: Если кто-то входит в лифт, подождите 15 секунд, если кто-то снова войдет, повторите время на 15 секунд, пока никто не войдет после 15-секундного тайм-аута, начните доставку.