Browse Source

feat: 新增 util - debounce (从 es-toolkit copy)

Utopia 6 months ago
parent
commit
18a8a2fe22
1 changed files with 166 additions and 0 deletions
  1. 166 0
      src/utils/debounce.ts

+ 166 - 0
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<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
+}