home ~ projects ~ socials

How To Make a Debounced Throttle in JavaScript

TL;DR

Neither debouncers or throttles do what I want by themselves. Throttles catch initial changes but miss the last one. Debouncers skip the first on in favor of catching the last.

This code combines them to get the effect I'm looking form. (An example demonstration and more details are further below.)

TODO: put in note about requestAnimationFrame()

JavaScript

class DebouncedThrottle {
  constructor() {
    this.slider = document.querySelector("#dt-slider");
    this.out1 = document.querySelector("#dt-output-1");
    this.out2 = document.querySelector("#dt-output-2");
    this.addListeners();
  }

  addListeners() {
    this.slider.addEventListener('input', (event) => {
      this.debouncedThrottle.call(
        this, 
        [event, this.makeUpdate1, 500]
      );
      this.debouncedThrottle.call(
        this, 
        [event, this.makeUpdate2, 1500]
      );
    }) 
    this.slider.addEventListener('change', (event) => {
      this.clearTimeouts.call(
        this, [event, this.makeUpdate1]
      );
      this.clearTimeouts.call(
        this, [event, this.makeUpdate2]
      );
    })
  }

  clearTimeouts(params) {
    const event = params[0];
    const fn = params[1];
    const name = fn.name;
    this.timers[name] = [null, null]
    fn.call(this, event);
  }

  debouncedThrottle(params) {
    const event = params[0];
    const fn = params[1];
    const name = fn.name;
    const msDelay = params[2];
    const now = new Date();
    if (!this.timers) {
      this.timers = {}
    }
    if (!this.timers[name]) {
      this.timers[name] = [null, null]
    }
    const timers = this.timers[name];
    if (timers[0] === null) {
      fn.call(this, event);
      timers[0] = new Date();
    } 
    if (now - timers[0] > msDelay) {
        fn.call(this, event);
        timers[0] = new Date();
    } else {
      if (timers[1]) {
        window.clearTimeout(timers[1]);
      }
      timers[1] = window.setTimeout(() => {
        fn.call(this, event);
        timers[0] = null;
      }, msDelay);
    }
  }

  makeUpdate1(event) {
    this.out1.innerHTML = `Value 1: ${event.target.value}`;
  }

  makeUpdate2(event) {
    this.out2.innerHTML = `Value 2: ${event.target.value}`;
  }
}

const debouncedThrottle = new DebouncedThrottle();

Do It Live

This example uses the code above.

Value 1: Waiting
Value 2: Waiting

Throttle By Itself Isn't Good

It picks up the changes immediately but missing the last ones unless it's at the exact right millisecond.

Output

Waiting

HTML

<div id="to-output">Waiting</div>
<div>
<label for="to-slider">Throttle Only Slider</label>
<input 
  id="to-slider"
  type="range" 
  name="to-slider"
/>

Debounce By Itself Isn't Good

This doesn't update the value until you've stopped moving the slider for 1 second.

JavaScript

class DebounceOnly {
  constructor() {
    this.slider = document.querySelector("#do-slider");
    this.output = document.querySelector("#do-output");
    this.addListener();
  }

  addListener() {
    this.slider.addEventListener('input', (event) => {
      this.doUpdate.call(this, event);
    }) 
  }

  doUpdate(event) {
    if (window.doUpdateId) {
      window.clearTimeout(window.doUpdateId);
    }
    window.doUpdateId = window.setTimeout(() => {
      this.output.innerHTML = event.target.value;
    }, 1000);
  }

}

const debouceOnly = new DebounceOnly();

Output

Waiting

HTML

<div id="do-output">Waiting</div>
<div>
<label for="do-slider">Debug Only Slider</label>
<input 
  id="do-slider"
  type="range" 
  name="do-slider"
/>
/*
function throttle(func, timeFrame) {
  var lastTime = 0;
  return function (...args) {
      var now = new Date();
      if (now - lastTime >= timeFrame) {
          func(...args);
          lastTime = now;
      }
  };
}
/*

/*
  debounce(callback, wait) {
    const localThis = this;
    console.log("asdf");
    let timeoutId = null;
    return (...args) => {
      window.clearTimeout(timeoutId);
      timeoutId = window.setTimeout(() => {
      //this.doUpdate(this);
        // callback.apply(localThis, args);
      }, wait);
    };
  }


  queueUpdate(event) {
    this.debounce(this.doUpdate, 2000);
  }
  */


/*
    */

    //let doUpdateId = null;

    // this.doUpdateId = "asdf";
    //window.clearTimeout(doUpdateId);

//console.log(this.doUpdateId);

    // console.log("----");
    //console.log(this.slider.value);
-- end of line --