Created
April 7, 2022 09:54
-
-
Save OysteinAmundsen/6fad0b3d9092da10a4d3ff4a2b3afc48 to your computer and use it in GitHub Desktop.
Debounce decorator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Debounce function decorator | |
* | |
* @param delay | |
* @param immediate | |
* @returns the function debounced | |
*/ | |
export function Debouncer(delay = 200, immediate = false): MethodDecorator { | |
return function (target: Record<string, unknown>, propertyKey: string | symbol, descriptor: PropertyDescriptor) { | |
const map = new WeakMap(); | |
const originalMethod = descriptor.value; | |
descriptor.value = function (...params: any[]) { | |
let method = map.get(this); | |
if (!method) { | |
method = debounce(originalMethod, delay, immediate); | |
map.set(this, method); | |
} | |
const resultPromise = method.result(); | |
const debounced = method.bind(this); | |
debounced(...params); | |
return resultPromise; | |
}; | |
return descriptor; | |
} as MethodDecorator; | |
} | |
/** | |
* Slightly modified version of lodash.debounce: https://github.com/lodash/lodash/blob/master/debounce.js | |
* | |
* This version returns a shared promise to all callers of the function, which will resolve all calls | |
* with the functions result when it is received. Then it will reset, so that the next calls will not be | |
* polluted with the previous result. | |
* | |
*/ | |
export function debounce<T extends (...args: any[]) => any>(func: T, wait = 200, options?: any) { | |
let lastArgs: any; | |
let lastThis: any; | |
let maxWait: number | undefined; | |
let result: T; | |
let resolver: (value: T | PromiseLike<T>) => void; | |
let rejector: (reaons?: any) => void; | |
let resultPromise = createResultPromise(); | |
let timerId: number | undefined; | |
let lastCallTime: number | undefined; | |
let lastInvokeTime = 0; | |
let leading = false; | |
let maxing = false; | |
let trailing = true; | |
// Bypass `requestAnimationFrame` by explicitly setting `wait=0`. | |
const useRAF = !wait && wait !== 0 && typeof window.requestAnimationFrame === 'function'; | |
if (typeof func !== 'function') throw new TypeError('Expected a function'); | |
wait = +wait || 0; | |
if (isObject(options)) { | |
leading = !!options.leading; | |
maxing = 'maxWait' in options; | |
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait; | |
trailing = 'trailing' in options ? !!options.trailing : trailing; | |
} | |
/** | |
* Called when all calls are in, and it is time to actually invoke the debounced function. | |
* | |
* @param time | |
* @returns | |
*/ | |
function invokeFunc(time: number): T { | |
const args = lastArgs; | |
const thisArg = lastThis; | |
lastArgs = lastThis = undefined; | |
lastInvokeTime = time; | |
result = func.apply(thisArg, args); | |
resolver(result); // Resolve the promise to all listeners | |
setTimeout(() => (resultPromise = createResultPromise())); // Reset the result promise for next run | |
return result; | |
} | |
function startTimer(pendingFunc: FrameRequestCallback, wait: number): number { | |
if (useRAF && timerId) { | |
window.cancelAnimationFrame(timerId); | |
return window.requestAnimationFrame(pendingFunc); | |
} | |
return setTimeout(pendingFunc, wait); | |
} | |
function cancelTimer(id: number): void { | |
if (useRAF) { | |
return window.cancelAnimationFrame(id); | |
} | |
clearTimeout(id); | |
} | |
function leadingEdge(time: number): T { | |
// Reset any `maxWait` timer. | |
lastInvokeTime = time; | |
// Start the timer for the trailing edge. | |
timerId = startTimer(timerExpired, wait); | |
// Invoke the leading edge. | |
return leading ? invokeFunc(time) : result; | |
} | |
function remainingWait(time: number): number { | |
const timeSinceLastCall = time - (lastCallTime || 0); | |
const timeSinceLastInvoke = time - lastInvokeTime; | |
const timeWaiting = wait - timeSinceLastCall; | |
return maxing ? Math.min(timeWaiting, (maxWait || 0) - timeSinceLastInvoke) : timeWaiting; | |
} | |
function shouldInvoke(time: number): boolean { | |
const timeSinceLastCall = time - (lastCallTime || 0); | |
const timeSinceLastInvoke = time - lastInvokeTime; | |
// Either this is the first call, activity has stopped and we're at the | |
// trailing edge, the system time has gone backwards and we're treating | |
// it as the trailing edge, or we've hit the `maxWait` limit. | |
return ( | |
lastCallTime === undefined || | |
timeSinceLastCall >= wait || | |
timeSinceLastCall < 0 || | |
(maxing && timeSinceLastInvoke >= (maxWait || 0)) | |
); | |
} | |
function timerExpired(): T | undefined { | |
const time = Date.now(); | |
if (shouldInvoke(time)) { | |
return trailingEdge(time); | |
} | |
// Restart the timer. | |
timerId = startTimer(timerExpired, remainingWait(time)); | |
return; | |
} | |
function trailingEdge(time: number): T { | |
timerId = undefined; | |
// Only invoke if we have `lastArgs` which means `func` has been | |
// debounced at least once. | |
if (trailing && lastArgs) { | |
return invokeFunc(time); | |
} | |
lastArgs = lastThis = undefined; | |
return result; | |
} | |
function createResultPromise() { | |
return new Promise<T>((resolve, reject) => { | |
resolver = resolve; | |
rejector = reject; | |
}); | |
} | |
function debounced(...args: any[]): T { | |
const time = Date.now(); | |
const isInvoking = shouldInvoke(time); | |
lastArgs = args; | |
lastThis = this; | |
lastCallTime = time; | |
if (isInvoking) { | |
if (timerId === undefined) { | |
return leadingEdge(lastCallTime); | |
} | |
if (maxing) { | |
// Handle invocations in a tight loop. | |
timerId = startTimer(timerExpired, wait); | |
return invokeFunc(lastCallTime); | |
} | |
} | |
if (timerId === undefined) { | |
timerId = startTimer(timerExpired, wait); | |
} | |
return result; | |
} | |
debounced.cancel = function cancel(): void { | |
if (timerId !== undefined) { | |
cancelTimer(timerId); | |
} | |
lastInvokeTime = 0; | |
lastArgs = lastCallTime = lastThis = timerId = undefined; | |
}; | |
debounced.flush = function flush(): T { | |
return timerId === undefined ? result : trailingEdge(Date.now()); | |
}; | |
debounced.pending = function pending(): boolean { | |
return timerId !== undefined; | |
}; | |
debounced.result = function () { | |
return resultPromise; | |
}; | |
return debounced; | |
} | |
function isObject(value: any): boolean { | |
const type = typeof value; | |
return value != null && (type === 'object' || type === 'function'); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment