debouncing - Vad är funktionen "debounce" i JavaScript?
Jag är intresserad av "debouncing"-funktionen i JavaScript, på JavaScript Debounce Function.
Tyvärr är koden inte förklarad tillräckligt tydligt för att jag ska förstå. Hur fungerar den (jag lämnade mina kommentarer nedan)? Kort sagt, jag förstår helt enkelt inte hur detta fungerar.
// 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);
};
};
Den kopierade kodsnutten hade tidigare callNow på fel plats.
Svar #1
Koden i frågan ändrades något från koden i länken. I länken finns det en kontroll för (immediate && !timeout) innan en ny timeout skapas. Att ha den efter gör att omedelbart-läge aldrig aktiveras. Jag har uppdaterat mitt svar för att kommentera den fungerande versionen från länken.
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);Svar #2
Det viktiga att notera här är att debounce producerar en funktion som är "sluten över" timeout-variabeln. timeout-variabeln förblir åtkomlig under varje anrop av den producerade funktionen även efter att debounce själv har returnerat, och kan ändras över olika anrop.
Den allmänna idén för debounce är följande:
- Börja utan timeout.
- Om den producerade funktionen anropas, rensa och återställ timeouten.
- Om tidsgränsen nås, anropa den ursprungliga funktionen.
Den första punkten är bara var timeout;, det är faktiskt bara undefined. Som tur är är clearTimeout ganska slapp med sin input: att skicka en undefined timeridentifierare gör att den helt enkelt inte gör någonting, den kastar inget fel eller något.
Den andra punkten görs av den producerade funktionen. Den lagrar först viss information om anropet (kontexten this och arguments) i variabler så att den senare kan använda dessa för det debounced anropet. Den rensar sedan timeouten (om det fanns en satt) och skapar sedan en ny för att ersätta den med hjälp av setTimeout. Observera att detta skriver över värdet på timeout och detta värde kvarstår över flera funktionsanrop! Detta tillåter debounce-funktionen att faktiskt fungera: om funktionen anropas flera gånger skrivs timeout över flera gånger med en ny timer. Om detta inte vore fallet skulle flera anrop orsaka att flera timers startas som alla förblir aktiva - anropen skulle helt enkelt försenas, men inte debounced.
Den tredje punkten görs i timeout-callbacken. Den avmarkerar timeout-variabeln och gör det faktiska funktionsanropet med hjälp av den lagrade anropsinformationen.
Flaggan immediate är avsedd att styra om funktionen ska kallas före eller efter timern. Om den är false kallas inte den ursprungliga funktionen förrän efter att timern slagits. Om den är true kallas den ursprungliga funktionen först och kommer inte att kallas mer förrän timern har slagits.
Jag tror dock att kontrollen if (immediate && !timeout) är fel: timeout har precis satts till timer-identifieraren som returneras av setTimeout, så !timeout är alltid false vid det tillfället och därför kan funktionen aldrig kallas. Den nuvarande versionen av underscore.js verkar ha en något annorlunda kontroll, där den utvärderar immediate && !timeout innan setTimeout kallas. (Algoritmen är också lite annorlunda, t.ex. använder den inte clearTimeout.) Därför bör du alltid försöka använda den senaste versionen av dina bibliotek. :-)
Svar #3
Debouncerade funktioner körs inte när de anropas. De väntar på en paus i anrop under en konfigurerbar tid innan de körs; varje nytt anrop startar om timern.
Strypade funktioner körs och väntar sedan en konfigurerbar tid innan de kan köras igen.
Debounce är bra för tangenttryckningsevenemang; när användaren börjar skriva och sedan pausar skickar du in alla tangenttryckningar som ett enda evenemang, vilket minskar antalet hanteringsanrop.
Throttle är utmärkt för realtidsändpunkter som du endast vill tillåta användaren att anropa en gång per en bestämd tidsperiod.
Kolla in Underscore.js för deras implementationer också.
Svar #4
Jag förstod inte heller fullt ut hur en debounce-funktion fungerade när jag först stötte på en sådan. Även om de är relativt små till storleken, använder de faktiskt några ganska avancerade JavaScript-koncept! Att ha bra koll på scope, closures och setTimeout-metoden kommer att hjälpa.
Med det sagt, nedan är den grundläggande debounce-funktionen förklarad och demonstrerad i mitt inlägg som refereras till ovan.
Den färdiga produkten
// 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);
}
};
};
Förklaringen
// 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);
}
};
};
Svar #5
vi använder alla Promises nu
Många implementationer jag har sett komplicerar problemet i onödan eller har andra hygienproblem. Det är 2021 och vi har använt Promises länge nu - och med god anledning också. Promises städar upp asynkrona program och minskar möjligheterna för misstag att uppstå. I detta inlägg kommer vi att skriva vår egen debounce. Denna implementation kommer att -
- ha högst ett löfte som väntar vid varje given tidpunkt (per avtrappad uppgift)
- stoppa minnesläckor genom att korrekt avbryta väntande löften
- lös endast det senaste löftet
- visa korrekt beteende med livekodsexempel
Vi skriver debounce med dess två parametrar, task som ska debouncas, och mängden millisekunder för fördröjning, ms. Vi introducerar en enda lokal bindning för dess lokala tillstånd, t -
function debounce (task, ms) {
let t = { promise: null, cancel: _ => void 0 }
return async (...args) => {
try {
t.cancel()
t = deferred(ms)
await t.promise
await task(...args)
}
catch (_) { /* prevent memory leak */ }
}
}
Vi är beroende av en återanvändbar deferred-funktion, som skapar ett nytt löfte som löses på ms millisekunder. Den introducerar två lokala bindningar, själva promise, och möjligheten att cancel det -
function deferred (ms) {
let cancel, promise = new Promise((resolve, reject) => {
cancel = reject
setTimeout(resolve, ms)
})
return { promise, cancel }
}
exempel på klickräknare
I detta första exempel har vi en knapp som räknar användarens klick. Händelselyssnaren är kopplad med hjälp av debounce, så räknaren ökas endast efter en angiven tid.
// debounce, deferred
function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } }
function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }
// dom references
const myform = document.forms.myform
const mycounter = myform.mycounter
// event handler
function clickCounter (event) {
mycounter.value = Number(mycounter.value) + 1
}
// debounced listener
myform.myclicker.addEventListener("click", debounce(clickCounter, 1000))<form id="myform">
<input name="myclicker" type="button" value="click" />
<output name="mycounter">0</output>
</form>exempel på liveförfrågan, "automatisk komplettering"
I det här andra exemplet har vi ett formulär med ett textinmatningsfält. Vår search-fråga är kopplad med hjälp av debounce -
// debounce, deferred
function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } }
function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }
// dom references
const myform = document.forms.myform
const myresult = myform.myresult
// event handler
function search (event) {
myresult.value = `Searching for: ${event.target.value}`
}
// debounced listener
myform.myquery.addEventListener("keypress", debounce(search, 1000))<form id="myform">
<input name="myquery" placeholder="Enter a query..." />
<output name="myresult"></output>
</form>flera avfyrningar, react hook useDebounce
I en annan Q&A frågar någon om det är möjligt att använda och exponera debounce-avbokningsmekanismen och skapa en useDebounce React-hook. Genom att använda deferred ovan är det en trivial övning.
// revised implementation
function debounce(task, ms) {
let t = { promise: null, cancel: _ => void 0 }
return [
// ...,
_ => t.cancel() // ✅ return cancellation mechanism
]
}
// revised usage
const [inc, cancel] = debounce(clickCounter, 1000) // ✅ two controls
myform.mybutton.addEventListener("click", inc)
myform.mycancel.addEventListener("click", cancel)
Att implementera en useDebounce React-hook är en barnlek -
function useDebounce(task, ms) {
const [f, cancel] = debounce(task, ms)
useEffect(_ => cancel) // ✅ auto-cancel when component unmounts
return [f, cancel]
}
Gå över till den ursprungliga Q&A för en komplett demo
