Может кто-нибудь объяснить функцию "debounce" в Javascript
Меня интересует функция "debouncing" в javascript, написанная здесь: http://davidwalsh.name/javascript-debounce-function
К сожалению, код недостаточно четко объяснен для меня, чтобы понять. Может кто-нибудь помочь мне разобраться, как это работает (я оставил свои комментарии ниже). Короче я просто очень не понимаю как это работает
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds.
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
РЕДАКТИРОВАТЬ: скопированный фрагмент кода ранее имел callNow
в неправильном месте.
13 ответов
Код в вопросе был немного изменен по сравнению с кодом в ссылке. В ссылке есть проверка на (immediate && !timeout)
ДО создания нового тайм-аута. После этого немедленный режим никогда не срабатывает. Я обновил свой ответ, чтобы аннотировать рабочую версию по ссылке.
function debounce(func, wait, immediate) {
// 'private' variable for instance
// The returned function will be able to reference this due to closure.
// Each call to the returned function will share this common timer.
var timeout;
// Calling debounce returns a new anonymous function
return function() {
// reference the context and args for the setTimeout function
var context = this,
args = arguments;
// Should the function be called now? If immediate is true
// and not already in a timeout then the answer is: Yes
var callNow = immediate && !timeout;
// This is the basic debounce behaviour where you can call this
// function several times, but it will only execute once
// [before or after imposing a delay].
// Each time the returned function is called, the timer starts over.
clearTimeout(timeout);
// Set the new timeout
timeout = setTimeout(function() {
// Inside the timeout function, clear the timeout variable
// which will let the next execution run when in 'immediate' mode
timeout = null;
// Check if the function already ran with the immediate flag
if (!immediate) {
// Call the original function with apply
// apply lets you define the 'this' object as well as the arguments
// (both captured before setTimeout)
func.apply(context, args);
}
}, wait);
// Immediate mode and no wait timer? Execute the function..
if (callNow) func.apply(context, args);
};
};
Здесь важно отметить, что debounce
производит функцию, которая "закрыта" над timeout
переменная. timeout
переменная остается доступной при каждом вызове производимой функции даже после debounce
сама вернулась и может переключаться на разные звонки.
Общая идея для debounce
является следующим:
- Начните без перерыва.
- Если вызванная функция вызвана, очистите и сбросьте тайм-аут.
- Если истекло время ожидания, вызовите оригинальную функцию.
Первый пункт просто var timeout;
это действительно просто undefined
, К счастью, clearTimeout
довольно слабо о его вводе: передача undefined
идентификатор таймера заставляет его просто ничего не делать, он не выдает ошибку или что-то еще.
Второй пункт сделан произведенной функцией. Сначала хранится некоторая информация о звонке (this
контекст и тому arguments
) в переменных, чтобы потом использовать их для отклоненного вызова. Затем он очищает тайм-аут (если был один набор), а затем создает новый, чтобы заменить его, используя setTimeout
, Обратите внимание, что это перезаписывает значение timeout
и это значение сохраняется в течение нескольких вызовов функций! Это позволяет debounce фактически работать: если функция вызывается несколько раз, timeout
перезаписывается несколько раз с новым таймером. Если бы это было не так, многократные вызовы привели бы к запуску нескольких таймеров, все из которых оставались бы активными - вызовы были бы просто отложены, но не отменены.
Третий пункт делается в обратном вызове тайм-аута. Это сбрасывает timeout
переменная и выполняет фактический вызов функции, используя сохраненную информацию о вызове.
immediate
Флаг должен контролировать, должна ли функция вызываться до или после таймера. Если это false
, оригинальная функция не вызывается до тех пор, пока не будет нажата таймер. Если это true
, оригинальная функция вызывается первой и больше не будет вызываться, пока не будет нажата таймер.
Тем не менее, я считаю, что if (immediate && !timeout)
проверка неверна: timeout
только что был установлен идентификатор таймера, возвращаемый setTimeout
так !timeout
всегда false
в этот момент и, следовательно, функция никогда не может быть вызвана. Текущая версия underscore.js, кажется, имеет немного другую проверку, где она оценивает immediate && !timeout
перед звонком setTimeout
, (Алгоритм также немного отличается, например, он не использует clearTimeout
Вот почему вы всегда должны использовать последнюю версию своих библиотек.:-)
Отказавшиеся функции не выполняются при вызове, они ждут паузу вызовов в течение настраиваемой продолжительности перед выполнением; каждый новый вызов перезапускает таймер.
Дросселированные функции выполняются, а затем ждут настраиваемую длительность, прежде чем снова смогут запускаться.
Debounce отлично подходит для событий нажатия клавиш; когда пользователь начинает печатать, а затем приостанавливает, вы отправляете все нажатия клавиш как одно событие, таким образом сокращая количество вызовов обработки.
Throttle отлично подходит для конечных точек в реальном времени, которые вы хотите разрешить пользователю вызывать только один раз за определенный период времени.
Проверьте http://underscorejs.org/ для их реализации тоже.
Я написал пост под названием " Демистификация Debounce в JavaScript", где я объясняю, как именно работает функция debounce, и включаю демонстрацию.
Я тоже не до конца понимал, как работает функция debounce, когда я впервые столкнулся с ней. Хотя они относительно небольшие по размеру, они на самом деле используют довольно продвинутые концепции JavaScript! Имея хороший контроль над областью, затвором и setTimeout
метод поможет.
С учетом вышесказанного ниже приведена базовая функция debounce, которая поясняется и демонстрируется в моем посте, на который мы ссылаемся выше.
Готовый продукт
// Create JD Object
// ----------------
var JD = {};
// Debounce Method
// ---------------
JD.debounce = function(func, wait, immediate) {
var timeout;
return function() {
var context = this,
args = arguments;
var later = function() {
timeout = null;
if ( !immediate ) {
func.apply(context, args);
}
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait || 200);
if ( callNow ) {
func.apply(context, args);
}
};
};
Объяснение
// Create JD Object
// ----------------
/*
It's a good idea to attach helper methods like `debounce` to your own
custom object. That way, you don't pollute the global space by
attaching methods to the `window` object and potentially run in to
conflicts.
*/
var JD = {};
// Debounce Method
// ---------------
/*
Return a function, that, as long as it continues to be invoked, will
not be triggered. The function will be called after it stops being
called for `wait` milliseconds. If `immediate` is passed, trigger the
function on the leading edge, instead of the trailing.
*/
JD.debounce = function(func, wait, immediate) {
/*
Declare a variable named `timeout` variable that we will later use
to store the *timeout ID returned by the `setTimeout` function.
*When setTimeout is called, it retuns a numeric ID. This unique ID
can be used in conjunction with JavaScript's `clearTimeout` method
to prevent the code passed in the first argument of the `setTimout`
function from being called. Note, this prevention will only occur
if `clearTimeout` is called before the specified number of
milliseconds passed in the second argument of setTimeout have been
met.
*/
var timeout;
/*
Return an anomymous function that has access to the `func`
argument of our `debounce` method through the process of closure.
*/
return function() {
/*
1) Assign `this` to a variable named `context` so that the
`func` argument passed to our `debounce` method can be
called in the proper context.
2) Assign all *arugments passed in the `func` argument of our
`debounce` method to a variable named `args`.
*JavaScript natively makes all arguments passed to a function
accessible inside of the function in an array-like variable
named `arguments`. Assinging `arguments` to `args` combines
all arguments passed in the `func` argument of our `debounce`
method in a single variable.
*/
var context = this, /* 1 */
args = arguments; /* 2 */
/*
Assign an anonymous function to a variable named `later`.
This function will be passed in the first argument of the
`setTimeout` function below.
*/
var later = function() {
/*
When the `later` function is called, remove the numeric ID
that was assigned to it by the `setTimeout` function.
Note, by the time the `later` function is called, the
`setTimeout` function will have returned a numeric ID to
the `timeout` variable. That numeric ID is removed by
assiging `null` to `timeout`.
*/
timeout = null;
/*
If the boolean value passed in the `immediate` argument
of our `debouce` method is falsy, then invoke the
function passed in the `func` argument of our `debouce`
method using JavaScript's *`apply` method.
*The `apply` method allows you to call a function in an
explicit context. The first argument defines what `this`
should be. The second argument is passed as an array
containing all the arguments that should be passed to
`func` when it is called. Previously, we assigned `this`
to the `context` variable, and we assigned all arguments
passed in `func` to the `args` variable.
*/
if ( !immediate ) {
func.apply(context, args);
}
};
/*
If the value passed in the `immediate` argument of our
`debounce` method is truthy and the value assigned to `timeout`
is falsy, then assign `true` to the `callNow` variable.
Otherwise, assign `false` to the `callNow` variable.
*/
var callNow = immediate && !timeout;
/*
As long as the event that our `debounce` method is bound to is
still firing within the `wait` period, remove the numerical ID
(returned to the `timeout` vaiable by `setTimeout`) from
JavaScript's execution queue. This prevents the function passed
in the `setTimeout` function from being invoked.
Remember, the `debounce` method is intended for use on events
that rapidly fire, ie: a window resize or scroll. The *first*
time the event fires, the `timeout` variable has been declared,
but no value has been assigned to it - it is `undefined`.
Therefore, nothing is removed from JavaScript's execution queue
because nothing has been placed in the queue - there is nothing
to clear.
Below, the `timeout` variable is assigned the numerical ID
returned by the `setTimeout` function. So long as *subsequent*
events are fired before the `wait` is met, `timeout` will be
cleared, resulting in the function passed in the `setTimeout`
function being removed from the execution queue. As soon as the
`wait` is met, the function passed in the `setTimeout` function
will execute.
*/
clearTimeout(timeout);
/*
Assign a `setTimout` function to the `timeout` variable we
previously declared. Pass the function assigned to the `later`
variable to the `setTimeout` function, along with the numerical
value assigned to the `wait` argument in our `debounce` method.
If no value is passed to the `wait` argument in our `debounce`
method, pass a value of 200 milliseconds to the `setTimeout`
function.
*/
timeout = setTimeout(later, wait || 200);
/*
Typically, you want the function passed in the `func` argument
of our `debounce` method to execute once *after* the `wait`
period has been met for the event that our `debounce` method is
bound to (the trailing side). However, if you want the function
to execute once *before* the event has finished (on the leading
side), you can pass `true` in the `immediate` argument of our
`debounce` method.
If `true` is passed in the `immediate` argument of our
`debounce` method, the value assigned to the `callNow` variable
declared above will be `true` only after the *first* time the
event that our `debounce` method is bound to has fired.
After the first time the event is fired, the `timeout` variable
will contain a falsey value. Therfore, the result of the
expression that gets assigned to the `callNow` variable is
`true` and the function passed in the `func` argument of our
`debounce` method is exected in the line of code below.
Every subsequent time the event that our `debounce` method is
bound to fires within the `wait` period, the `timeout` variable
holds the numerical ID returned from the `setTimout` function
assigned to it when the previous event was fired, and the
`debounce` method was executed.
This means that for all subsequent events within the `wait`
period, the `timeout` variable holds a truthy value, and the
result of the expression that gets assigned to the `callNow`
variable is `false`. Therefore, the function passed in the
`func` argument of our `debounce` method will not be executed.
Lastly, when the `wait` period is met and the `later` function
that is passed in the `setTimeout` function executes, the
result is that it just assigns `null` to the `timeout`
variable. The `func` argument passed in our `debounce` method
will not be executed because the `if` condition inside the
`later` function fails.
*/
if ( callNow ) {
func.apply(context, args);
}
};
};
мы все сейчас используем обещания
Многие реализации, которые я видел, чрезмерно усложняют проблему или имеют другие проблемы с гигиеной. Это 2021 год, и мы уже давно используем Promises - и на то есть веские причины. Обещания очищают асинхронные программы и уменьшают вероятность ошибок. В этом посте мы напишем свое. Эта реализация будет -
- иметь не более одного ожидающего выполнения обещания в любой момент времени (для каждой отклоненной задачи)
- остановить утечку памяти, правильно отменив ожидающие обещания
- разрешить только последнее обещание
- продемонстрировать правильное поведение с помощью демонстраций живого кода
Мы пишем с двумя параметрами:
task
для устранения неполадок, и количество миллисекунд для задержки,. Мы вводим единую локальную привязку для его локального состояния,
t
-
function debounce (task, ms) {
let t = { promise: null, cancel: _ => void 0 }
return async (...args) => {
try {
t.cancel()
t = deferred()
await t.promise
await task(...args)
}
catch (_) { /* prevent memory leak */ }
}
}
Мы зависим от многоразового
deferred
функция, которая создает новое обещание, которое разрешается в
ms
миллисекунды. Он вводит две локальные привязки:
promise
сама способность
cancel
Это -
function deferred (ms) {
let cancel, promise = new Promise((resolve, reject) => {
cancel = reject
setTimeout(resolve, ms)
})
return { promise, cancel }
}
пример счетчика кликов
В этом первом примере у нас есть кнопка, которая подсчитывает клики пользователя. Слушатель событий прикрепляется с помощью
debounce
, поэтому счетчик увеличивается только после указанной продолжительности -
Простая функция устранения дребезга:-
HTML:-
<button id='myid'>Click me</button>
Javascript:-
function debounce(fn, delay) {
let timeoutID;
return function(...args){
if(timeoutID) clearTimeout(timeoutID);
timeoutID = setTimeout(()=>{
fn(...args)
}, delay);
}
}
document.getElementById('myid').addEventListener('click', debounce(() => {
console.log('clicked');
},2000));
Что вы хотите сделать, это следующее: если вы пытаетесь вызвать функцию сразу за другой, первая должна быть отменена, а новая должна подождать заданное время ожидания и затем выполнить. Таким образом, в действительности вам нужен способ отменить таймаут первой функции? Но как? Вы можете вызвать функцию и передать возвращаемый таймаут-идентификатор, а затем передать этот идентификатор в любые новые функции. Но решение, приведенное выше, более элегантно.
Что он делает, так это эффективно timeout
переменная доступна в объеме возвращаемой функции. Поэтому, когда происходит событие "resize", оно не вызывает debounce()
опять же отсюда timeout
содержимое не изменяется (!) и все еще доступно для "следующего вызова функции".
Ключевым моментом здесь является то, что мы вызываем внутреннюю функцию каждый раз, когда происходит событие изменения размера. Возможно, будет более понятно, если мы представим, что все события resize находятся в массиве:
var events = ['resize', 'resize', 'resize'];
var timeout = null;
for (var i = 0; i < events.length; i++){
if (immediate && !timeout) func.apply(this, arguments);
clearTimeout(timeout); // does not do anything if timeout is null.
timeout = setTimeout(function(){
timeout = null;
if (!immediate) func.apply(this, arguments);
}
}
Вы видите timeout
доступно для следующей итерации? И нет никаких причин, по моему мнению, переименовать this
в content
а также arguments
в args
,
Это вариант, который всегда запускает заблокированную функцию при первом вызове с более описательными именами переменных:
function debounce(fn, wait = 1000) {
let debounced = false;
let resetDebouncedTimeout = null;
return function(...args) {
if (!debounced) {
debounced = true;
fn(...args);
resetDebouncedTimeout = setTimeout(() => {
debounced = false;
}, wait);
} else {
clearTimeout(resetDebouncedTimeout);
resetDebouncedTimeout = setTimeout(() => {
debounced = false;
fn(...args);
}, wait);
}
}
};
Предположим, что пользователь печатает предложение наinput
элемент со скоростью 5 символов в секунду, и вы слушаетеkeyup
для запроса API с частичным текстом, написанным пользователем. Чтобы сэкономить ресурсы, вы хотите вызывать API не при каждом нажатии клавиши, а через определенные промежутки времени или после того, как какое-то время не будет обнаружено нажатий клавиш. Это устраняет дребезг и показано на GIF-изображении ниже: вход становится красным всякий раз, когда срабатывает обратный вызов.
GIF показывает четыре разных подхода:
- Никакого отскока.
- Отложите вызов и отмените предыдущие отложенные вызовы.
debounce(func, 300)
- Если возможно, позвоните немедленно и отложите будущие звонки. В противном случае отложите звонок.
debounce(func, 300, { immediate: true })
- Задержка плюс некоторые принудительные интервалы для непрерывной обратной связи, если пользователь вводит длинные предложения без пауз.
debounce(func, 300, { interval: 1500 })
Для этого примераimmediate=false
имеет больше смысла, но для кнопки это имеет больше смысла.
function debounce(func, delay, { immediate = false, interval = 1e30 } = {}) {
let awaiting = false;
let last_params, deadline_A, deadline_B = 1e30;
async function deadlineSleep() {
while (true) {
if (deadline_B + delay < Date.now()) deadline_B = 1e30;
let ms = Math.min(deadline_A, deadline_B) - Date.now();
if (ms <= 0) return;
await new Promise(resolve => setTimeout(resolve, ms));
}
}
async function wrapper(...args) {
last_params = { arg0: this, args };
deadline_A = Date.now() + delay;
if (awaiting) return;
awaiting = true;
if (!immediate) await deadlineSleep();
while (last_params) {
const { arg0, args } = last_params;
last_params = null;
deadline_B = Date.now() + interval;
try { await func.apply(arg0, args); }
catch (e) { console.error(e); }
await deadlineSleep();
}
awaiting = false;
};
return wrapper;
}
Формально,wrapper = debounce(func, delay, {immediate, interval})
соблюдает эти правила:
- Всегда звоните как можно раньше, с последними полученными параметрами, соблюдая при этом остальные правила.
- Если асинхронно, никакие два вызова не выполняются параллельно.
- Самый последний вызов всегда вызывает вызов .
- Не звоните больше одного раза в окно мс.
- Задержка, если последний (или предпоследний, если
immediate=true
) позвонитьwrapper
был внутриdelay
мс назад. В противном случае выполните немедленно. - Если существует цепочка отложенных вызовов, на которую приходится
interval
мс, отмените предыдущее правило и выполнитеfunc
немедленно.
Простой метод Debounce в javascript
<!-- Basic HTML -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Debounce Method</title>
</head>
<body>
<button type="button" id="debounce">Debounce Method</button><br />
<span id="message"></span>
</body>
</html>
// JS File
var debouncebtn = document.getElementById('debounce');
function debounce(func, delay){
var debounceTimer;
return function () {
var context = this, args = arguments;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
func.apply(context, args)
}, delay);
}
}
// Driver Code
debouncebtn.addEventListener('click', debounce(function() {
document.getElementById('message').innerHTML += '<br/> Button only triggeres is every 3 secounds how much every you fire an event';
console.log('Button only triggeres in every 3 secounds how much every you fire an event');
},3000))
Пример выполнения JSFiddle: https://jsfiddle.net/arbaazshaikh919/d7543wqe/10/
Ниже приведено краткое изложение того, что делает функция, объясненное в нескольких строках с демонстрацией.
А
debounce
функция — это функция, которая будет:
- при первом выполнении запланируйте выполнение обернутой функции через определенный интервал времени с помощью функции
- (если выполняется снова в течение этого интервала):
- удалить предыдущее расписание (с
clearTimeOut
функция) - перепланировать новый (с помощью функции setTimeoutsetTimeout )
- удалить предыдущее расписание (с
И цикл продолжается до тех пор, пока не истечет интервал времени и не выполнится обернутая функция.
Адаптировано из всех комментариев и из этой статьи
Функция устранения дребезга в стиле Promise делает код более линейным и понятным, поэтому вам не нужно переносить вызовы:
async doSomething(params) {
if (await debounce(this.doSomething)) return
// doSomething...
}
утилиты.js:
export function debounce(func, delay = 100) {
var promise = defer()
if (func.pendingCall) {
clearTimeout(func.pendingCall.timeout)
func.pendingCall.promise.resolve(true)
}
func.pendingCall = {
promise,
timeout: setTimeout(_ => promise.resolve(false), delay)
}
return promise
}
export function defer() {
var methods = {}, complete = false
var promise = new Promise(function(resolve, reject) {
methods.resolve = _ => {
if (!complete) resolve(_)
complete = true
}
methods.reject = _ => {
if (!complete) reject(_)
complete = true
}
})
Object.assign(promise, methods)
return promise
}
Если вы используетеreact.js
function debounce(func, delay = 600) {
return (args) => {
clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
func(args);
}, delay);
};
}
const triggerSearch = debounce(handleSearch);
// Event which triggers search.
onSearch={(searchedValue) => {
setSearchedText(searchedValue);// state update
triggerSearch(searchedValue);
}}
Из-за этого обновления состояния в событии поиска, которое запускается для каждого типа буквы, это было повторное отображение, и весь код сdebounce func
также был повторно инициирован.
Из-за такого поведения реакции никогда не было активного тайм-аута.
function debounce(func, delay = 600) {
return (args) => {
clearTimeout(timeout.current);
timeout.current = setTimeout(() => {
func(args);
}, delay);
};
}
const triggerSearch = debounce(handleSearch);
Чтобы исправить это, я использовалref
названныйtimeout
.
const timeout = useRef();