If I use _.debounce() to make a debounced function and then call it 3 times in succession with 3 different sets of arguments, then (as of v1.1.7) the wrapped payload function will finally be called with the arguments specified by the 3rd call - that is the first and second arguments are discarded.
While this is often valid (and what key debouncing typically does, hence a reasonable default) I find myself wanting to use debounce to accumulate arguments, for example I have an AJAX call that can get multiple keys at once, so I use debounce to buffer up keys for a second and then issue a combined request.
My suggestion is therefore that debounce takes an optional 3rd "combine" argument that will be called with 2 args,
If no value is passed for the combine parameter, the default combine preserves existing behaviour
function(acc, newargs) { return newargs; }
but you can also decide to use the first set of arguments
function(acc, newargs) { return acc || newargs; }
or what I want to do which is simply append all the arguments
function(acc,newargs) { return (acc || []).concat(newargs); }
and of course others may want to do something fancier
This would require the following change to the internal limit function
// Internal function used to implement `_.throttle` and `_.debounce`.
var limit = function(func, wait, debounce, combine) {
var timeout, allargs;
return function() {
var context = this;
allargs = combine(allargs, slice.call(arguments,0))
var throttler = function() {
timeout = null;
var args = allargs;
allargs = undefined;
func.apply(context, args);
};
if (debounce) clearTimeout(timeout);
if (debounce || !timeout) timeout = setTimeout(throttler, wait);
};
};
and then a change to debounce to accept and pass thru the new argument with default value if not specified.
_.debounce = function(func, wait, combine) {
return limit(func, wait, true, combine || function(acc,newargs) { return newargs; });
};
The corresponding throttle function currently uses the first set of arguments alone (throttle effectively ignores calls happening within wait milliseconds of a first call and uses the first call set of args, debounce effectively ignores all but the last call in a sequence occurring within the wait period of each other), so I'd suggest the below to again preserve the current default behaviour
_.throttle = function(func, wait, combine) {
return limit(func, wait, false, combine || function(acc,newargs) { return acc || newargs; });
};
This would seem the easiest and most general way to achieve this functionality without excessive wrappers to maintain the argument lists, but I'd be interested to know if there's an easy way to achieve this without changing underscore.
Commenting on my own suggestion, the call to combine() should specify the same context as the payload function, hence
allargs = combine.apply(this, [allargs, slice.call(arguments,0)])
in case the arguments need to access the context object....
Should now be fixed on master. throttle
should exhibit the correct behavior where it always uses the latest copy of your arguments, fires once immediately, and every N
seconds thereafter ... and resets itself N
seconds after the last trailing trigger has occurred.
I think your close comment applies to another issue (probably #170), as the issue raised by this request still applies on master.
There's still no easy way to have debounce or throttle accumulate arguments from the calls that are being combined, and I still think it's a useful optional addition which leaves the default behaviour unchanged when the optional combining argument is not specified.
Ah, you're right. Accumulating arguments is outside of the scope of Underscore -- feel free to stash your accumulated data in a good place external to the _.throttle
and _.debounce
functions.
That's a pity, I consider debounce to be a sort of fold-left (reduce) over multiple calls with timeout hence the accumulator... but it's your call :)
OK, not to keep banging on, but in case anyone's looking at this sometime later and wondering how to do the same, I figured this was about the cleanest way without modifying debounce itself (I add it to the _ object, others may prefer not to)
_.mixin({
debounceReduce: function(func, wait, combine) {
var allargs,
context,
wrapper = _.debounce(function() {
var args = allargs;
allargs = undefined;
func.apply(context, args);
}, wait);
return function() {
context = this;
allargs = combine.apply(context, [allargs, Array.prototype.slice.call(arguments,0)]);
wrapper();
};
}
})
This gives a debounced function that has its arguments reduced by the combine function so, for example,
delayLog = _.debounceReduce(function() { console.log(arguments); }, 5000,
function(acc,args) { return (acc || []).concat(args); });
delayLog(3,4);
delayLog(7,8,9);
will a few second later call console.log with the array [3,4,7,8,9]
@schmerg — this looks tremendously useful. Would you be willing to license that code under the MIT license? (A "yes" will suffice!)
@markjaquith Sure thing - yes. Glad to...
If anyone comes along and wants updated/commented modern js version of the above:
_.mixin({
debounceReduce(func, wait, combine) {
let allArgs; // accumulator for args across calls
// normally-debounced fn that we will call later with the accumulated args
const wrapper = _.debounce(() => func(allArgs), wait);
// what we actually return is this function which will really just add the new args to
// allArgs using the combine fn
return (...args) => {
allArgs = combine(allArgs, [...args]);
wrapper();
};
},
});
@kmannislands Hey, your version doesn't reset allArgs
inside wrapper()
, so subsequent calls to func()
get historical batches of args as well as the current batch.
Shouldn't it be:
const wrapper = _.debounce(() => {
const args = allArgs;
allArgs = undefined;
func(args);
}, wait);
@markjaquith +1
Also func( args )
differs from the original version that uses func.apply(context, args);
.
For the former, args
is used as is in the the target func()
, whereas in the later _(original code)_ you need to use either arguments
in a normal function or ( ...args )
in an es6 fat arrow function.
Most helpful comment
OK, not to keep banging on, but in case anyone's looking at this sometime later and wondering how to do the same, I figured this was about the cleanest way without modifying debounce itself (I add it to the _ object, others may prefer not to)
This gives a debounced function that has its arguments reduced by the combine function so, for example,
will a few second later call console.log with the array [3,4,7,8,9]