I want to throttle my function every 100 milliseconds. In the following code, I expect only 1 and 3 to be printed. But 2 is also printed in the actual result.
function say(what) {
console.log(what);
}
const t = _.throttle(say, 100);
setTimeout(() => {
t(1);
}, 50);
setTimeout(() => {
t(1);
}, 50);
setTimeout(() => {
t(1);
}, 50);
setTimeout(() => {
t(1);
}, 55);
setTimeout(() => {
t(2);
}, 55);
setTimeout(() => {
t(3);
}, 500);
<script src=".js/4.11.1/lodash.js"></script>
I want to throttle my function every 100 milliseconds. In the following code, I expect only 1 and 3 to be printed. But 2 is also printed in the actual result.
function say(what) {
console.log(what);
}
const t = _.throttle(say, 100);
setTimeout(() => {
t(1);
}, 50);
setTimeout(() => {
t(1);
}, 50);
setTimeout(() => {
t(1);
}, 50);
setTimeout(() => {
t(1);
}, 55);
setTimeout(() => {
t(2);
}, 55);
setTimeout(() => {
t(3);
}, 500);
<script src="https://cdnjs.cloudflare./ajax/libs/lodash.js/4.11.1/lodash.js"></script>
I have to change the throttle wait time to 500 in order to filter out the 2.
Maybe my understanding of throttle is wrong. Shouldn't throttle execute the function at most one time per wait period?
Share Improve this question edited May 13, 2019 at 21:22 Derek Pollard 7,1756 gold badges43 silver badges60 bronze badges asked May 13, 2019 at 21:17 techguy2000techguy2000 5,1916 gold badges38 silver badges53 bronze badges 5-
1
The call to
t(2)
is the last call made during the first 100ms. It'll be called when the 100ms expires. If you add the current timestamp (Date.now()
) to the log messages you'll see that the call tot(2)
happens almost exactly 100ms after the firstt(1)
call. – Pointy Commented May 13, 2019 at 21:26 - The feels a lot like an XY problem. Mind sharing the exact issue instead of this vague representation? – Derek Pollard Commented May 13, 2019 at 21:31
- @Pointy so the first function is called always, and only the subsequent functions are spread out by wait time. Is that right? – techguy2000 Commented May 13, 2019 at 21:33
-
1
@techguy2000 unless you set
trailing
tofalse
-_.throttle(say, 100, { trailing: false });
This would log1
and then3
– Brett East Commented May 13, 2019 at 21:37 -
alternatively, you could set
leading
tofalse
and it would log2
and then3
and not log1
at all – Brett East Commented May 13, 2019 at 21:39
3 Answers
Reset to default 4Your understanding of how you are using this throttle setup is not quite right.
To directly answer:
Shouldn't throttle execute the function at most one time per wait period?
With no options passed in, throttle will execute at the beginning and the end of the wait period (twice), provided the throttled function was called more than once in that period.
At 50ms your first function is called, and 'throttle' runs it immediately, also at this time your next f(1) is queued up to be called at 100ms. But then another f(1) is called, and another f(1) and then an f(2), and each new one replaces the last as the function to be called at 100ms (which is the time you've passed into throttle). Then over 100ms passes and then f(3) is called more or less when it should be.
If you don't pass any options into _.throttle
it will immediately call the first function run (at 0ms) and then will call the last function run inside the set time period once that time elapses.
Using @zfrisch's code as a start:
function say(what) {
console.log(what);
}
const t = _.throttle(say, 100);
const TO = (n, i) => setTimeout(() => {
t(n);
}, i);
TO(1, 50); // logged immediately
TO(1, 50);
TO(1, 50);
TO(1, 55);
TO(2, 55); // logged at 100ms (as it was the last function attempted)
function say(what) {
console.log(what);
}
const t = _.throttle(say, 100, { leading: false });
const TO = (n, i) => setTimeout(() => {
t(n);
}, i);
TO(1, 50); // not logged at all
TO(1, 50);
TO(1, 50);
TO(1, 55);
TO(2, 55); // logged at 100ms (as it was the last function attempted)
function say(what) {
console.log(what);
}
const t = _.throttle(say, 100, { trailing: false });
const TO = (n, i) => setTimeout(() => {
t(n);
}, i);
TO(1, 50); // logged immediately
TO(1, 50);
TO(1, 50);
TO(1, 55);
TO(2, 55); // not logged at all
Throttle will delay the execution by the wait time, but the function will still be called. In your example, it will happen like follows: 50ms after the script start the first 1 is logged, and at this moment throttle will start counting 100ms. Any call to t in this period will be delayed. After the 100ms passed, the last call made to t will be triggered. 100ms passes again and t is not called, so nothing happens, then the last call happens.
The Problem:
It's easy to see how you assumed your code would work. It's not a hard thing to misinterpret, but the reason why it doesn't execute the way you're expecting is because of the default behavior of the function.
The Throttle
Function:
From the above you can see that Throttle
takes a function
and a wait
time in milliseconds as its first two parameters, but the options
are not used in your current code and those are the key to solving your issue.
The Options:
_.throttle
has the options leading
and trailing
. These two are both defaulted to true
.
Unfortunately the named options, though apt, are not crystal clear in purpose simply from their names unless you're familiar with the process that's happening behind the curtain.
An Explanation:
leading
refers to those that are called at the applicable times. These would be:
- the initial call
- the next call after 100ms
- the next call after another 100ms
- etc.
trailing
refers to the last call of the throttle
function made during the interval. This function is not simply discarded, but postponed until it can be called. Think of it as a slot that's available to be filled during the interval. When a call is made during the interval, that slot is filled/replaced with the new function.
This can be illustrated:
- Initial Call is Made
- A call with
function 1
is made during interval - A call with
function 2
is made during the interval function 2
replacesfunction 1
as the trailing function- At
100ms
function 2
is run.
An important concept, and ultimately the crux of what's happening in your code, is the following:
If both options are true
, the trailing function made during the interval will be called before a new leading call to the throttle
function.
Because of this the new leading call bees a trailing call for the end of the next interval.
The documentation that I've pulled direct from the lodash official site does specify this, but you would be forgiven for misunderstanding as it's a lot to prehend if you're not familiar with their terms:
*See the Throttle Documentation on Lodash
What does this mean?
In your example, when you call _.throttle
it will, by default, wait to perform the function call until the interval has passed and continuously rewrite the function waiting to be called during the 100ms
interval. To change this and only accept/perform the calls that are made after the interval, you can turn trailing
to false
.
A Working Solution:
function say(what) {
console.group();
console.log(what);
console.timeLog("check");
console.groupEnd();
}
const t = _.throttle(say, 100, {trailing: false});
const TO = (n, i) => setTimeout(()=>{t(n);}, i);
console.time("check");
TO(1,50);
TO(1,50);
TO(1,50);
TO(1,55);
TO(2,55);
TO(3,500);
<script src="https://cdnjs.cloudflare./ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>