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-секундного тайм-аута, начните доставку.
-