From 18a8a2fe224a28e8ebde37c52c27196a010d0402 Mon Sep 17 00:00:00 2001 From: Utopia Date: Fri, 31 Oct 2025 14:20:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20util=20-=20debounc?= =?UTF-8?q?e=20(=E4=BB=8E=20es-toolkit=20copy)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/debounce.ts | 166 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 src/utils/debounce.ts diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts new file mode 100644 index 0000000..c13f470 --- /dev/null +++ b/src/utils/debounce.ts @@ -0,0 +1,166 @@ +// 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 void> { + (...args: Parameters): 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 void>( + func: F, + debounceMs: number, + { signal, edges }: DebounceOptions = {}, +): DebouncedFunction { + let pendingThis: any + let pendingArgs: Parameters | 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 | 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) { + 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 +}