11

Underscore provides the method, throttle. From their docs:

Creates and returns a new, throttled version of the passed function, that, when invoked repeatedly, will only actually call the original function at most once per every wait milliseconds. Useful for rate-limiting events that occur faster than you can keep up with.

Now imagine the case of an autocomplete form. This means that if 'abc' is typed within a, say, 100ms window, then only a search for 'a' will be sent, and not 'bc'.

Is this a drastic oversight on the part of underscore.js? What would you suggest as a clean solution?

Ross
  • 14,266
  • 12
  • 60
  • 91
Peter Ehrlich
  • 6,969
  • 4
  • 49
  • 65

6 Answers6

5

For this use case, you may want to use the following "buffer" function, which will apply only the last call within the wait window.

https://gist.github.com/2484196

_.buffer = function(func, wait, scope) {
  var timer = null;
  return function() {
    if(timer) clearTimeout(timer);
    var args = arguments;
    timer = setTimeout(function() {
      timer = null;
      func.apply(scope, args);
    }, wait);
  };
};
pedroteixeira
  • 806
  • 7
  • 7
  • 1
    Nice solution, +1. Still, I'd not add it to the `_` object because this may confuse someone that reads your code. *"Was this method from an older version of Underscore? Is this a [Lo-Dash](http://lodash.com/) extension?"* – Camilo Martin Sep 13 '14 at 23:52
3

Ah, I didn't read the docs right! There is a function for this.

var test = _.debounce(function() { console.log('foo'); }, 3000);

Then if you call test() a few times you'll notice that only after three seconds since the last call, will the function ever be called.

This is exactly what we both were looking for... and didn't notice was right below throttle on the docs.

Underscore docs, Lo-Dash docs (what is LoDash?)

Community
  • 1
  • 1
Camilo Martin
  • 37,236
  • 20
  • 111
  • 154
  • 3
    While this seems to work in this use case, debounce and throttle give a different behavior. I'm looking for a way to keep the requests going through while typing, but still ensure the last call (as in the question here). – Joe Hidakatsu Jun 14 '21 at 02:52
2

It looks like the other answers mention using debounce instead of throttle. I found a solution that allows you to keep throttle behavior (keeping function calls going through) while still ensuring the last call.

https://gist.github.com/ayushv512/a2f963bface38f5e2f6f6bba39bba9b9#file-throttle-js

 const throttle = (func, limit) => {
  let lastFunc;
  let lastRan;
  return function() {
    const context = this;
    const args = arguments;
    if (!lastRan) {
      func.apply(context, args)
      lastRan = Date.now();
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(function() {
          if ((Date.now() - lastRan) >= limit) {
            func.apply(context, args);
            lastRan = Date.now();
          }
       }, limit - (Date.now() - lastRan));
    }
  }
}
Joe Hidakatsu
  • 464
  • 4
  • 16
  • This solution is kinda bad/wasteful. The first if condiiton should be replaced with the if condiiton in the setTimeout. ``lastRan`` is only before the first call falsy, so ``if (!lastRan)`` doesn't make sense anymore after that. Conversely the ``if ((Date.now() - lastRan) >= limit)`` in the setTimeout is out of place because you set a timeout to call the function when it is allowed to be called again and therefore doesn't need to be gated again. This if condition should hence be replaced with the ``if (!lastRan)`` so that you only set a timeout when the function was called inbetween frames – Ilja KO Aug 27 '23 at 01:46
2

Late to the party but the accepted answer is a debounce function not a throttle + I have a more versatile throttle function which can act as a normal or a debounced version to ensure the last call gets fired after the delay is over. So essentially combining throttle and debounce into one if you wish to

function throttle(f, delay = 0, ensure = false) {
  let lastCall = Number.NEGATIVE_INFINITY;
  let wait;
  let handle;
  return (...args) => {
    wait = lastCall + delay - Date.now();
    clearTimeout(handle);
    if (wait <= 0 || ensure) {
      handle = setTimeout(() => {
        f(...args);
        lastCall = Date.now();
      }, wait);
    }
  };
}

How it works:

  • It is foremost a simple throttle function, where you can call it if the last call was longer ago than the delay
  • You can set a flag ensure to make the throttle function ensure the last call will get called if it was called in between "frames"
    • For example if you throttle a function to 1 call per second it means it will only fire when the last call is longer than 1 second ago
  • If you call the throttle function with the ensure flag enabled while the last call is e.g. less than 1 second away, e.g. 600ms, then the throttled function simply sets a timeout to call the function in 400ms (wait = lastCall + delay - Date.now() - here if lastCall = 0 and delay = 1000 and Date.now() = 600 then the time out will happen in 400)
  • Otherwise if you wish to treat it as a simple throttle function just don't set the ensure flag and calls in between "frames" will just be ignored.

I think this is an elegant solution to add more functionality to a simple throttle function. I use it to move things around in an editor while keeping the redrawing at a certain framerate but still make sure that movements that should have happened in between frames are still ensured to be applied

Ilja KO
  • 1,272
  • 12
  • 26
1

I keep an idle function around to handle this type of user interaction. The idea is to require some function to be called at a regular interval (i.e, when a key is pressed in an input); as soon as the interval passes without a call to said function, a separate callback is triggered. This might not be quite the behavior you want for your autocomplete (you might want to start searching before the user's input has paused), but it's one approach I've used in the past.

If anyone has a better solution for this, though, I'm all ears!

rjz
  • 16,182
  • 3
  • 36
  • 35
  • that's good to know (especially the _ mixin!), but not exactly the same- I definitely want results to appear while people type. – Peter Ehrlich Mar 08 '12 at 05:29
  • Yeah, I hear you. I think the tricky part is deciding where to draw the line with respect to the user's typing speed and the delay in getting a response from the AJAX request. Without some kind of googlesque pre-fetch, it seems like the request will (necessarily) lag behind user input. More pondering needed :-) – rjz Mar 08 '12 at 05:35
0

Yes, you are right that it won't work well for an autocomplete form. I think it's meant for other types of things, though. The example given is handling scroll events.

nicholaides
  • 19,211
  • 12
  • 66
  • 82