Gunther
Gunther

Reputation: 2645

Debounce function in jQuery

I'm attempting to debounce a button's input using the jquery debouncing library by Ben Alman. http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/

Currently this is the code that I have.

function foo() {
    console.log("It works!")
};

$(".my-btn").click(function() {
    $.debounce(250, foo);
});

The problem is that when I click the button, the function never executes. I'm not sure if I've misunderstood something but as far as I can tell, my code matches the example.

Upvotes: 44

Views: 98710

Answers (3)

Mr. Polywhirl
Mr. Polywhirl

Reputation: 48600

Here are modernized (ES6+) versions of Ben Alman's debounce and throttle functions with more versatile and readable signatures compared to the original jquery-throttle-debounce plugin.

I noticed that the plugin hasn't changed in 15 years (and counting), so I rewrote them to use a more modern approach.

Debounce

Ensures that a function only runs once after a specified delay, even if triggered multiple times in rapid succession. This is useful for scenarios like input validation or search suggestions, where you want the function to run only after the user stops typing.

/**
 * Creates a debounced version of a function that delays invoking the function until 
 * after the specified wait time has elapsed since the last time it was invoked.
 *
 * @param {Function} fn - The function to debounce.
 * @param {number} wait - The delay in milliseconds after the last call before invoking the function.
 * @param {boolean} [immediate=false] - If true, the function is triggered at the beginning of the delay period instead of at the end.
 * @returns {Function} - Returns the debounced function.
 */
const debounce = (fn, wait, immediate = false) => {
  let timer;
  return function (...args) {
    const callNow = immediate && !timer;
    clearTimeout(timer);
    timer = setTimeout(() => {
      timer = null;
      if (!immediate) fn.apply(this, args);
    }, wait);
    if (callNow) fn.apply(this, args);
  };
};

Throttle

Limits the rate at which a function executes, ensuring it only runs once within each specified interval. This is ideal for scenarios like scroll or resize events, where excessive function calls could degrade performance.

/**
 * Creates a throttled version of a function that limits its invocation to at most once 
 * every specified interval.
 *
 * @param {Function} fn - The function to throttle.
 * @param {number} wait - The interval in milliseconds during which the function should only be invoked once.
 * @param {Object} [options={}] - Configuration options for throttling behavior.
 * @param {boolean} [options.leading=true] - If true, the function will be triggered at the start of the interval.
 * @param {boolean} [options.trailing=true] - If true, the function will be triggered at the end of the interval.
 * @returns {Function} - Returns the throttled function.
 */
const throttle = (fn, wait, options = {}) => {
  const { leading = true, trailing = true } = options;
  let last = 0, timer;
  return function (...args) {
    const now = performance.now();
    if (!last && !leading) last = now;
    const remaining = wait - (now - last);
    if (remaining <= 0 || remaining > wait) {
      if (timer) clearTimeout(timer);
      last = now;
      fn.apply(this, args);
    } else if (trailing && !timer) {
      timer = setTimeout(() => {
        last = leading ? performance.now() : 0;
        timer = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
};

These updates improve flexibility and clarity, making it easier to customize timing behavior:

  • Options Object: Clear properties (leading, trailing, and immediate) replace positional booleans, making configurations simpler to read and adjust.
  • Precision with Modern JavaScript: Uses performance.now() for high-precision timing and modern syntax (like destructuring and arrow functions) for compatibility with ES6+ standards.
  • Flexible Default Behavior: throttle defaults to executing on both leading and trailing edges, while debounce waits until the delay ends, giving better out-of-the-box behavior with easy customization.

These improvements make debounce and throttle more intuitive and versatile, adaptable for jQuery or native JS environments.

Demo

I included a demonstration of these plugins below. There are two buttons using the same click handler; one debounced and the other throttled.

Spam-click each button to see how they behave.

Note: I included a $.withPreventDefault plugin as well. This is useful because when you wrap a function in a debounce or throttle, it is hard to intercept the event in case you want to prevent the default behavior.

const disableMenu = (e) => e.preventDefault();

const onClick = function() {
  $(this).increment(); // Custom increment function
};

$(() => {
  $(document).on('contextmenu', '.counter', disableMenu)

  // Only increment after 250 ms of inactivity
  $('#debounced')
    .on('mousedown', $.debounce($.withPreventDefault(onClick), 250, true));

  // Only allow increment once per second
  $('#throttled')
    .on('mousedown', $.throttle($.withPreventDefault(onClick), 1000));
});

// Custom increment function
(($) => {
  $.fn.increment = function(amount = 1) {
    const $button = $(this);
    const currentText = $button.text();
    const currentValue = parseInt($button.val() || '0', 10);
    const newValue = currentValue + amount;
    $button
      .text(currentText.replace(/(?<=\()\d+(?=\)$)/, newValue))
      .val(newValue);
  };
})(jQuery);

// Add modern throttle and debounce functions to jQuery
(($) => {
  const debounce = (fn, wait, immediate = false) => {
    let timer;
    return function(...args) {
      const callNow = immediate && !timer;
      clearTimeout(timer);
      timer = setTimeout(() => {
        timer = null;
        if (!immediate) fn.apply(this, args);
      }, wait);
      if (callNow) fn.apply(this, args);
    };
  };

  const throttle = (fn, wait, options = {}) => {
    const { leading = true, trailing = true } = options;
    let last = 0, timer;
    return function(...args) {
      const now = performance.now();
      if (!last && !leading) last = now;
      const remaining = wait - (now - last);
      if (remaining <= 0 || remaining > wait) {
        if (timer) clearTimeout(timer);
        last = now;
        fn.apply(this, args);
      } else if (trailing && !timer) {
        timer = setTimeout(() => {
          last = leading ? performance.now() : 0;
          timer = null;
          fn.apply(this, args);
        }, remaining);
      }
    };
  };
  
  const withPreventDefault = (fn) => function (event, ...args) {
    if (event && typeof event.preventDefault === 'function') {
      event.preventDefault();
    }
    return fn.apply(this, [event, ...args]);
  };

  $.extend({ debounce, throttle, withPreventDefault });
})(jQuery);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<button type="button" id="debounced" class="counter" value="0">Debounced (0)</button>
<button type="button" id="throttled" class="counter" value="0">Throttled (0)</button>

Upvotes: 3

Geoff
Geoff

Reputation: 886

I ran into the same issue. The problem is happening because the debounce function returns a new function which isn't being called anywhere.

To fix this, you will have to pass in the debouncing function as a parameter to the jquery click event. Here is the code that you should have.

$(".my-btn").click($.debounce(250, function(e) {
  console.log("It works!");
}));
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-throttle-debounce/1.1/jquery.ba-throttle-debounce.min.js"></script>
<button class="my-btn">Click me!</button>

Upvotes: 87

isapir
isapir

Reputation: 23493

In my case I needed to debounce a function call that was not generated directly by a jQuery event handler, and the fact that $.debounce() returns a function made it impossible to use, so I wrote a simple function called callOnce() that does the same thing as Debounce, but can be used anywhere.

You can use it by simply wrapping the function call with a call to callOnce(), e.g. callOnce(functionThatIsCalledFrequently); or callOnce(function(){ doSomething(); }

/**
 * calls the function func once within the within time window.
 * this is a debounce function which actually calls the func as
 * opposed to returning a function that would call func.
 * 
 * @param func    the function to call
 * @param within  the time window in milliseconds, defaults to 300
 * @param timerId an optional key, defaults to func
 */
function callOnce(func, within=300, timerId=null){
    window.callOnceTimers = window.callOnceTimers || {};
    if (timerId == null) 
        timerId = func;
    var timer = window.callOnceTimers[timerId];
    clearTimeout(timer);
    timer = setTimeout(() => func(), within);
    window.callOnceTimers[timerId] = timer;
}

Upvotes: 12

Related Questions