Reputation: 17132
I am trying to make a debounce function, and I do not understand why it doesn't debounce.
I first created this function here:
const debounce = function throttleFunctionCalls(func, wait, immediate) {
let timeout
return () => {
const context = this
const args = arguments
const later = () => {
timeout = null
if (!immediate) func.apply(context, args)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(context, args)
}
}
const testFunction = () => console.log('test')
setInterval(() => debounce(testFunction(), 10000), 100)
But, it logs every 100ms, so it is not working as intended.
I tried a completely different debounce function, which is arguably better due to spreading args rather than using arguments
, but it suffers the same problem:
function debounce(func, wait) {
let timeout
return function throttleFunctionCalls(...args) {
const context = this
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(context, args), wait)
}
}
const testFunction = () => console.log('test')
setInterval(() => debounce(testFunction(), 10000), 100)
I don't have a real scenario to debounce right now, so I'm having trouble testing it in its natural habitat.
Is there something wrong with the setInterval
approach?
It seems like its awareness of timeout
is getting lost every time. Can anyone shed some light?
[edit]
As charlietfl pointed out in a comment below, I should not be evoking the function. Oops, I definitely shouldn't be doing that, but I tried it before making this question and it didn't work, so for some reason, I ended up with what is above.
function debounce(func, wait) {
let timeout
return function throttleFunctionCalls(...args) {
const context = this
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(context, args), wait)
}
}
const testFunction = (value) => console.log('test: ' + value)
setInterval(() => debounce(testFunction, 1), 100)
This above hangs the thread when I just pass reference function as a reference. I am also uncertain how to pass the actual function arguments into it unless it picks them up by closure.
Can anyone show me a functional example that can pass an arbitrary value in with testFunction()?
Upvotes: 1
Views: 2160
Reputation: 19301
The code is more or less complete but is being called incorrectly. debounce
is returning a function that has been debounced. You have to call the debounced function instead of the function supplied to debounce
, or debounce
itself multiple times.
Note that for the original function to be called the first time, calls to the debounced function must stop for longer than the debounce period. The logic could be made more complicated to let calls through every now and then even if they never stop - but that is not generally how hardware debounce circuitry works.
Here's an example of your code in action:
function debounce(func, wait) {
let timeout
return function throttleFunctionCalls(...args) {
const context = this
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(context, args), wait)
}
}
const testFunction = (string) => console.log('test ' + string)
const debouncedTestFunction = debounce( testFunction, 500);
setTimeout( debouncedTestFunction, 100, "first call");
setTimeout( debouncedTestFunction, 200, "second call");
setTimeout( debouncedTestFunction, 300, "third call");
// and pause for a while
setTimeout( debouncedTestFunction, 900, "fourth call");
Because of the setTimeout
delays chosen for a 500ms debounce, the expected result is that the third and fourth calls should get through, but the first and second will not.
Credit to @charlietfl in question comment to not call testFunction
when a reference to the function is required.
You were wise to abandon the use of the arguments
array in the second version of debounce
posted. As noted in other answers arguments
it is not supported in arrow functions but the failure mode would have been interesting - the debounce
function expression starts with function
which introduces arguments
into function scope. The arrow function returned would then use the arguments
value of the debounce
call as in this example:
function test() {
return ()=>arguments[0]
}
console.log( (test("test argument"))("call argument"));
Quite a debugging mess. It would be of interest to check how babel translates the arrow function usage of the arguments
value.
A second concern is that the (second) debounce
function effectively returns a bound function (in the manner of Function.prototype.bind
) but records the this
value seen within the debounce
function whatever that may be. May I suggest either to add a thisValue
argument to debounce
or use null
for the thisValue
when calling apply
. (The exact effect of null
depends on whether the called code is in strict mode).
Upvotes: 1
Reputation: 664395
In the call
setInterval(() => debounce(testFunction(), 10000), 100)
you a) pass the return value of testFunction()
- undefined
- to debounce
, instead of the function itself and b) never call the function that is returned by debounce
. This is how the invocation should look like:
const debouncedTestFunction = debounce(testFunction, 10000)
setInterval(() => debouncedTestFunction(), 100)
or in short
setInterval(debounce(testFunction, 10000), 100)
That said, you are misusing arrow syntax. Arrow functions do not have their own arguments
and this
values but inherit them lexically, so your code doesn't work. It needs to be
function debounce(func, wait, immediate) {
let timeout
return function() {
// ^^^^^^^^^^
const later = () => {
timeout = null
if (!immediate) func.apply(this, arguments)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(this, arguments)
}
}
Upvotes: 2
Reputation: 1846
Let's look through your last example:
function debounce(func, wait) {
let timeout
return function throttleFunctionCalls(...args) {
const context = this
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(context, args), wait)
}
}
You've defined a function debounce
that takes in a function and a wait time and returns a function. So far so good.
const testFunction = (value) => console.log('test: ' + value)
Next you've defined a function testFunction
that takes in a value and logs it.
setInterval(() => debounce(testFunction, 1), 100)
Here's a problem. As the first argument to setInterval
you have created a new function that takes in no arguments and returns the result of calling debounce
on testFunction
. As stated above, debounce
will simply return a function. Effectively, once every 100 milliseconds, you're just returning a new function rather than executing the logging logic.
Here's a slightly more condensed version of your code using arrow functions:
const debounce = (func, wait) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
};
Again we'll construct a test function:
const testFunction = (value) => {
console.log(`test: ${value}`);
};
This time we'll make a debounced version of the test function with a wait time of 1ms:
const debouncedTest1 = debounce(testFunction, 1);
And now we can set an interval to execute it. This is also one of the ways you can pass arguments to the test function:
setInterval(debouncedTest1, 200, 'foo');
This will log 'foo' every 200ms. Technically the first log message will come up 201ms after the setInterval
call: 200ms for the interval, and 1ms for the debounce logic. No debouncing will occur here because the interval is longer than the debounce wait period.
Trying this the other way around will not output anything:
const debouncedTest2 = debounce(testFunction, 200);
setInterval(debouncedTest2, 1, 'bar');
After 1ms, the interval timeout will attempt to call debouncedTest2
. The debounce logic will cause it to begin waiting 200ms before executing the log. But 1ms later, the next interval timeout will fire, which will push back that debounce interval again. This continues indefinitely, and you never see any messages.
In truth, trying to test debounce with setInterval
may not be the greatest approach.
Let's just try a more human-observable wait period and manually call the debounced function several times in rapid succession:
const debouncedTest3 = debounce(testFunction, 1000);
debouncedTest3('hi');
debouncedTest3('hello');
debouncedTest3('hey');
If you attempt to run this test, it will wait about one second and then output test: hey
. The first two calls were thrown away by the debounce logic.
On a tangent, another way to pass the arguments to your test function is to bake them into the debounced function call. There are multiple ways you could approach this. Here are two examples:
const debouncedTest4 = debounce(testFunction, 1000);
const debouncedTest4WithArg = () => debouncedTest4('test arg');
debouncedTest4WithArg();
debouncedTest4WithArg();
debouncedTest4WithArg();
const debouncedTest5 = debounce(() => testFunction('test arg'), 1000);
debouncedTest5();
debouncedTest5();
debouncedTest5();
Upvotes: 2