You can also implement your own debounce in vanilla JavaScript. A widely quoted article is David Walsh's article on function debouncing with underscore which includes the source code used by underscore in their implementation:
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
function debounce(func, wait, immediate) {
    var timeout;
    return function() {
        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
};
The debounce function serves as a generator for the actual function you'd like to call, that way the state can be persisted inside of the closure like this:
// example function
let sayHello = (name) => console.log(`Hi ${name}`)
// generate a debounced version with a min time between calls of 2 seconds
let sayHelloDebounced = debounce(sayHello, 2000)
// call however you want
sayHelloDebounced('David')
Demo in Stack Snippets
function debounce(func, wait, immediate) {
 var timeout;
 return function() {
  var context = this, args = arguments;
  var later = function() {
   timeout = null;
   if (!immediate) func.apply(context, args);
  };
  var callNow = immediate && !timeout;
  clearTimeout(timeout);
  timeout = setTimeout(later, wait);
  if (callNow) func.apply(context, args);
 };
};
let sayHello = (name) => console.log(`Hi ${name}`)
let sayHelloDebounced = debounce(sayHello, 2000)
sayHelloDebounced('David')
sayHelloDebounced('David')
sayHelloDebounced('David')
 
 
Other Implementations