Skip to content

Instantly share code, notes, and snippets.

@getify
Last active August 26, 2019 16:15
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save getify/f55b2bcb4c90dc239918836fada0e98a to your computer and use it in GitHub Desktop.
Save getify/f55b2bcb4c90dc239918836fada0e98a to your computer and use it in GitHub Desktop.
Scheduler (debouncing + throttling) | https://codepen.io/getify/pen/LYPbmYG?editors=0012
// NOTE: To see this demo: https://codepen.io/getify/pen/LYPbmYG?editors=0012
var counter = 1;
function printMessage() {
console.log(`message ${counter++}`);
}
var schedule = Scheduler(/* debounceMinimum = */50,/* throttleMaximum = */500);
// try to schedule a message to be printed (after approx 50ms of debounce)
schedule(printMessage);
setTimeout(function waitAWhile(){
// try to schedule next message to be printed (after approx 50ms of debounce)
schedule(printMessage);
// but now keep flooding the scheduling, so it keeps debouncing, up to the 500ms max throttling
var intv = setInterval(function(){ schedule(printMessage); },30);
// stop the madness, after about 10 seconds!
setTimeout(function(){ clearInterval(intv); },10*1000);
},3*1000);
// "message 1" (printed after about 50ms)
// (waiting about 3.5 seconds)
// "message 2"
// "message 3" (after another 500ms)
// "message 4" (after another 500ms)
// ..
function Scheduler(debounceMin,throttleMax) {
var entries = new WeakMap();
return schedule;
// ***********************
function schedule(fn) {
var entry;
if (entries.has(fn)) {
entry = entries.get(fn);
}
else {
entry = {
last: 0,
timer: null,
};
entries.set(fn,entry);
}
var now = Date.now();
if (!entry.timer) {
entry.last = now;
}
if (
// no timer running yet?
entry.timer == null ||
// room left to debounce while still under the throttle-max?
(now - entry.last) < throttleMax
) {
if (entry.timer) {
clearTimeout(entry.timer);
}
let time = Math.min(debounceMin,Math.max(0,(entry.last + throttleMax) - now));
entry.timer = setTimeout(run,time,fn,entry);
}
}
function run(fn,entry) {
entry.timer = null;
entry.last = Date.now();
fn();
}
}
@ValeraS
Copy link

ValeraS commented Aug 26, 2019

@getify Should it throttle execution if we call schedule between debounceMin and throttleMax? For example, if change line 20 in 1.js like this:

  var intv = setInterval(function(){ schedule(printMessage); },60); // 30 -> 60

IMHO, schedule could be something like this:

function schedule(fn) {
  var entry;

  if (entries.has(fn)) {
    entry = entries.get(fn);
  } else {
    entry = {
      last: 0,
      timer: null,
    };
    entries.set(fn, entry);
  }

  var now = Date.now();

  //if (!entry.timer) {
  //  entry.last = now;
  //}

  if (
    // no timer running yet?
    entry.timer == null ||
    // room left to debounce while still under the throttle-max?
   (now - entry.last) < throttleMax
  ) {
   let time = Math.min(debounceMin, Math.max(0, (entry.last + throttleMax) - now));
   if (entry.timer) {
     clearTimeout(entry.timer);
   } else if ((now - entry.last) < throttleMax) {
     // a function worked and throttle time did not expire
     return;
   } else {
     // the first run or throttle is timed out
     time = debounceMin;
   }

    entry.timer = setTimeout(run, time, fn, entry);
  }
}

@getify
Copy link
Author

getify commented Aug 26, 2019

Should it throttle execution if we call schedule between debounceMin and throttleMax?

Debouncing, by definition (in software, not hardware), defers an event if it's firing too quickly (dropping the intervening invocation(s) if so), and only fires it once, when things have settled down for at least the debounceMin amount of time. If you are only sending events on a less regular basis than debounceMin, then no debouncing should occur; each event should go out "on time".

The classic case for debouncing in UI software is if someone is scrolling a page, how often do you fire the "onscroll" event? You don't really need to fire a hundred times (when you can't render that quickly anyway); you only really need the event fired when the scrolling has finished, or at least paused briefly. So you debounce that event. Same thing if you were tracking coordinates of the mouse as it's moved quickly across the screen. Or if you were doing an autocomplete suggestion list as someone typed.

The reason I added throttleMax was because without an upper-bound on the deferral period, debouncing could theoretically keep going forever and never fire the event, if the event flooding just kept happening. throttleMax here ensures that the event happens at least once per that specified amount of time, even in the flooded debouncing scenario.

By contrast, "throttling" by definition ensures that the event fires at most once in a given time period. IOW, it's an upper-bound on the frequency. My throttleMax isn't really about "throttling" in that sense, because it's really an upper-bound on the debouncing deferral, not the frequency.

Perhaps the "throttle" name is what's confusing here? But I couldn't think what else to call that "max" limit besides "throttle" because it is very related to "once per time period".

@ValeraS
Copy link

ValeraS commented Aug 26, 2019

thank you for the detailed answer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment