Files
crm_uiapp/src/utils/debounce.ts
2026-04-14 15:06:26 +08:00

167 lines
4.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// fork from https://github.com/toss/es-toolkit/blob/main/src/function/debounce.ts
// 文档可查看https://es-toolkit.dev/reference/function/debounce.html
// 如需要 throttle 功能,可 copy https://github.com/toss/es-toolkit/blob/main/src/function/throttle.ts
interface DebounceOptions {
/**
* An optional AbortSignal to cancel the debounced function.
*/
signal?: AbortSignal
/**
* An optional array specifying whether the function should be invoked on the leading edge, trailing edge, or both.
* If `edges` includes "leading", the function will be invoked at the start of the delay period.
* If `edges` includes "trailing", the function will be invoked at the end of the delay period.
* If both "leading" and "trailing" are included, the function will be invoked at both the start and end of the delay period.
* @default ["trailing"]
*/
edges?: Array<'leading' | 'trailing'>
}
export interface DebouncedFunction<F extends (...args: any[]) => void> {
(...args: Parameters<F>): void
/**
* Schedules the execution of the debounced function after the specified debounce delay.
* This method resets any existing timer, ensuring that the function is only invoked
* after the delay has elapsed since the last call to the debounced function.
* It is typically called internally whenever the debounced function is invoked.
*
* @returns {void}
*/
schedule: () => void
/**
* Cancels any pending execution of the debounced function.
* This method clears the active timer and resets any stored context or arguments.
*/
cancel: () => void
/**
* Immediately invokes the debounced function if there is a pending execution.
* This method executes the function right away if there is a pending execution.
*/
flush: () => void
}
/**
* Creates a debounced function that delays invoking the provided function until after `debounceMs` milliseconds
* have elapsed since the last time the debounced function was invoked. The debounced function also has a `cancel`
* method to cancel any pending execution.
*
* @template F - The type of function.
* @param {F} func - The function to debounce.
* @param {number} debounceMs - The number of milliseconds to delay.
* @param {DebounceOptions} options - The options object
* @param {AbortSignal} options.signal - An optional AbortSignal to cancel the debounced function.
* @returns A new debounced function with a `cancel` method.
*
* @example
* const debouncedFunction = debounce(() => {
* console.log('Function executed');
* }, 1000);
*
* // Will log 'Function executed' after 1 second if not called again in that time
* debouncedFunction();
*
* // Will not log anything as the previous call is canceled
* debouncedFunction.cancel();
*
* // With AbortSignal
* const controller = new AbortController();
* const signal = controller.signal;
* const debouncedWithSignal = debounce(() => {
* console.log('Function executed');
* }, 1000, { signal });
*
* debouncedWithSignal();
*
* // Will cancel the debounced function call
* controller.abort();
*/
export function debounce<F extends (...args: any[]) => void>(
func: F,
debounceMs: number,
{ signal, edges }: DebounceOptions = {},
): DebouncedFunction<F> {
let pendingThis: any
let pendingArgs: Parameters<F> | null = null
const leading = edges != null && edges.includes('leading')
const trailing = edges == null || edges.includes('trailing')
const invoke = () => {
if (pendingArgs !== null) {
func.apply(pendingThis, pendingArgs)
pendingThis = undefined
pendingArgs = null
}
}
const onTimerEnd = () => {
if (trailing) {
invoke()
}
// eslint-disable-next-line ts/no-use-before-define
cancel()
}
let timeoutId: ReturnType<typeof setTimeout> | null = null
const schedule = () => {
if (timeoutId != null) {
clearTimeout(timeoutId)
}
timeoutId = setTimeout(() => {
timeoutId = null
onTimerEnd()
}, debounceMs)
}
const cancelTimer = () => {
if (timeoutId !== null) {
clearTimeout(timeoutId)
timeoutId = null
}
}
const cancel = () => {
cancelTimer()
pendingThis = undefined
pendingArgs = null
}
const flush = () => {
invoke()
}
const debounced = function (this: any, ...args: Parameters<F>) {
if (signal?.aborted) {
return
}
// eslint-disable-next-line ts/no-this-alias
pendingThis = this
pendingArgs = args
const isFirstCall = timeoutId == null
schedule()
if (leading && isFirstCall) {
invoke()
}
}
debounced.schedule = schedule
debounced.cancel = cancel
debounced.flush = flush
signal?.addEventListener('abort', cancel, { once: true })
return debounced
}