Может кто-нибудь объяснить функцию "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);
};
};
EDIT: скопированный фрагмент кода ранее имел callNow
в неправильном месте.
Ответы
Ответ 1
Код в вопросе был немного изменен по сравнению с кодом в ссылке. В ссылке есть проверка на (immediate && !timeout)
Timeout (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);
}
}
/////////////////////////////////
// DEMO:
function onMouseMove(e){
console.clear();
console.log(e.x, e.y);
}
// Define the debounced function
var debouncedMouseMove = debounce(onMouseMove, 50);
// Call the debounced function on every mouse move
window.addEventListener('mousemove', debouncedMouseMove);
Ответ 2
Важно отметить, что debounce
создает функцию, которая "закрыта" для переменной timeout
. Переменная timeout
остается доступной во время каждого вызова созданной функции даже после того, как сама debounce
вернулась и может меняться по разным вызовам.
Общая идея debounce
заключается в следующем:
- Начать без тайм-аута.
- Если вызывается вызванная функция, очистите и reset тайм-аут.
- Если время ожидания нажата, вызовите исходную функцию.
Первая точка - это просто 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
.) Вот почему вы всегда должны пытаться использовать последнюю версию своих библиотек.: -)
Ответ 3
Деблокированные функции не выполняются при вызове, они ожидают паузу вызовов в течение настраиваемой продолжительности перед выполнением; каждый новый вызов перезапускает таймер.
Дросселируемые функции выполняются, а затем ждут настраиваемой продолжительности, прежде чем снова смогут возобновить огонь.
Debounce отлично подходит для событий нажатия клавиш; когда пользователь начинает печатать, а затем делает паузу, вы отправляете все нажатия клавиш как одно событие, сокращая при этом обращения к обработке.
Throttle отлично подходит для конечных точек реального времени, которые вы только хотите разрешить пользователю вызывать один раз за определенный период времени.
Откажитесь от Underscore.js для своих реализаций.
Ответ 4
Я написал сообщение под названием Demistifying 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 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 `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 *`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 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 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);
}
};
};
Ответ 5
Что вы хотите сделать, так это следующее: если вы попытаетесь вызвать функцию сразу после другого, первый должен быть отменен, а новый должен дождаться заданного таймаута и затем выполнить. Таким образом, вам нужен какой-то способ отмены таймаута первой функции? Но как?
Вы можете вызвать функцию и передать возвращаемый тайм-аут, а затем передать этот идентификатор в любые новые функции. Но вышеприведенное решение более элегантно.
То, что он делает, эффективно делает переменную 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
.