Skip to content

Instantly share code, notes, and snippets.

@beaucharman
Last active November 12, 2022 15:39
Show Gist options
  • Save beaucharman/e46b8e4d03ef30480d7f4db5a78498ca to your computer and use it in GitHub Desktop.
Save beaucharman/e46b8e4d03ef30480d7f4db5a78498ca to your computer and use it in GitHub Desktop.
An ES6 implementation of the throttle function. "Throttling enforces a maximum number of times a function can be called over time. As in 'execute this function at most once every 100 milliseconds.'" - CSS-Tricks (https://css-tricks.com/the-difference-between-throttling-and-debouncing/)
function throttle(callback, wait, immediate = false) {
let timeout = null
let initialCall = true
return function() {
const callNow = immediate && initialCall
const next = () => {
callback.apply(this, arguments)
timeout = null
}
if (callNow) {
initialCall = false
next()
}
if (!timeout) {
timeout = setTimeout(next, wait)
}
}
}
/**
* Normal event
* event | | |
* time ----------------
* callback | | |
*
* Call search at most once per 300ms while keydown
* keydown | | | |
* time -----------------
* search | |
* |300| |300|
*/
const input = document.getElementById('id')
const handleKeydown = throttle((arg, event) => {
console.log(`${event.type} for ${arg} has the value of: ${event.target.value}`)
}, 300)
input.addEventListener('keydown', (event) => {
handleKeydown('input', event)
})
@undergroundwires
Copy link

Typescript version of throttle function with final and immediate invocations

// Throttle with ensured final and immediate invocations
const throttle = <T extends []> (callback: (..._: T) => void, wait: number): (..._: T) => void => {
    let queuedToRun: NodeJS.Timeout | undefined;
    let previouslyRun: number;
    return function invokeFn(...args: T) {
        const now = Date.now();
        queuedToRun = clearTimeout(queuedToRun) as undefined;
        if (!previouslyRun || (now - previouslyRun >= wait)) {
            callback(...args);
            previouslyRun = now;
        } else {
            queuedToRun = setTimeout(invokeFn.bind(null, ...args), wait - (now - previouslyRun));
        }
    };
};

Thanks to @robertmirro and @FRSgit

@FRSgit
Copy link

FRSgit commented Sep 19, 2021

Nice one @undergroundwires! But I've skipped leading invocation on purpose, because I was aiming for the simplest implementation. Also, I found out that having this immediate leading method firing is not always a good idea, but that of course depends on a use case.

If we want to write the "fullest" throttle fn I think there should be a possibility to opt out from leading & trailing callback calls. Exactly as they do in lodash.

@benneq
Copy link

benneq commented Nov 12, 2022

Here's my version, using TypeScript. It's returning a callback to cancel the timeout, useful if use have to use some cleanup function.

const throttle = <TArgs extends unknown[] = []>(
  callback: (...args: TArgs) => void
): ((ms: number, ...args: TArgs) => (() => void)) => {
  let timeout: NodeJS.Timeout | undefined;
  let lastArgs: TArgs;

  const cancel = () => {
    clearTimeout(timeout);
  };

  return (ms, ...args) => {
    lastArgs = args;

    if (!timeout) {
      timeout = setTimeout(() => {
        callback(...lastArgs);
        timeout = undefined;
      }, ms);
    }

    return cancel;
  };
};

Usage:

const cb = (n) => console.log(n);
const throttledCb = throttle(cb);
throttledCb(100, 1);
// wait 50ms
throttledCb(100, 2);
// wait 50ms
// prints 2

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