/*{ "parent": "utilities", "description": "Rate-limiting helpers: throttle() runs at most once per interval; debounce() delays calls until activity stops." }*/ /*# # throttle & debounce Usage: ``` const debouncedFunc = debounce(func, 250) const throttledFunc = debounce(func, 250) ``` `throttle(voidFunc, interval)` and `debounce(voidFunc, interval)` are utility functions for producing functions that filter out unnecessary repeated calls to a function, typically in response to rapid user input, e.g. from keystrokes or pointer movement. ```js import { throttle, debounce, on } from 'tosijs' function follow( element ) { return ( event ) => { element.style.top = event.offsetY + 'px' element.style.left = event.offsetX + 'px' } } on(preview, 'mousemove', follow(preview.querySelector('#unfiltered'))) on(preview, 'mousemove', throttle(follow(preview.querySelector('#throttle')))) on(preview, 'mousemove', debounce(follow(preview.querySelector('#debounce')))) ``` ```html
Move your mouse around in here…
follow function — triggers immediately
throttled follow function — triggers every 250ms
debounced follow function — stop moving for 250ms to trigger it
``` ```css .preview * { pointer-events: none; } .preview .follower { top: 100px; left: 400px; position: absolute; border-width: 4px; border-style: solid; background: transparent; transform: translateX(-50%) translateY(-50%); } ``` The usual purpose of these functions is to prevent over-calling of a function based on rapidly changing data, such as keyboard event or scroll event handling. `debounce`ed functions will only actually be called `interval` ms after the last time the wrapper is called. E.g. if the user types into a search field, you can call a `debounce`ed function to do the query, and it won't fire until the user stops typing for `interval` ms. `throttle`ed functions will only called at most every `interval` ms. E.g. if the user types into a search field, you can call a `throttle`ed function every `interval` ms, including one last time after the last time the wrapper is called. > In particular, both throttle and debounce are guaranteed to execute the > wrapped function after the last call to the wrapper. Note that parameters will be passed to the wrapped function, and that *the last call always goes through*. However, parameters passed to skipped calls will *never* reach the wrapped function. */ type VoidFunc = (...args: any[]) => void export const debounce = (origFn: VoidFunc, minInterval = 250): VoidFunc => { let debounceId: number // `function` (not an arrow) so `this` passes through to the wrapped // function — a debounced method invoked as obj.method() used to lose it return function (this: any, ...args: any[]) { if (debounceId !== undefined) clearTimeout(debounceId) debounceId = setTimeout(() => { origFn.apply(this, args) }, minInterval) as unknown as number } } export const throttle = (origFn: VoidFunc, minInterval = 250): VoidFunc => { let debounceId: number let previousCall = Date.now() - minInterval // `function` (not an arrow) so `this` passes through — see debounce return function (this: any, ...args: any[]) { clearTimeout(debounceId) const elapsed = Date.now() - previousCall if (elapsed >= minInterval) { // leading edge — no trailing timer: a lone call fires exactly once previousCall = Date.now() origFn.apply(this, args) } else { // suppressed — schedule the trailing call ("the last call always goes // through") for the moment the interval elapses, with the latest args debounceId = setTimeout(() => { previousCall = Date.now() origFn.apply(this, args) }, minInterval - elapsed) as unknown as number } } }