diff --git a/docs/ko/reference/function/debounce.md b/docs/ko/reference/function/debounce.md index 2339ba272..395714036 100644 --- a/docs/ko/reference/function/debounce.md +++ b/docs/ko/reference/function/debounce.md @@ -7,13 +7,19 @@ debounce된 함수는 또한 대기 중인 실행을 취소하는 `cancel` 메 ## 인터페이스 ```typescript -function debounce void>(func: F, debounceMs: number): F & { cancel: () => void }; +function debounce void>( + func: F, + debounceMs: number, + options?: DebounceOptions +): F & { cancel: () => void }; ``` ### 파라미터 - `func` (`F`): debounce된 함수를 만들 함수. - `debounceMs`(`number`): debounce로 지연시킬 밀리초. +- `options` (`DebounceOptions`, optional): 옵션 객체. + - `signal` (`AbortSignal`, optional): debounce된 함수를 취소하기 위한 선택적 `AbortSignal`. ### 결괏값 @@ -21,6 +27,8 @@ function debounce void>(func: F, debounceMs: numbe ## 예시 +### 기본 사용법 + ```typescript const debouncedFunction = debounce(() => { console.log('실행됨'); @@ -32,3 +40,23 @@ debouncedFunction(); // 이전 호출이 취소되었으므로, 아무것도 로깅하지 않아요 debouncedFunction.cancel(); ``` + +### AbortSignal 사용법 + +```typescript +const controller = new AbortController(); +const signal = controller.signal; +const debouncedWithSignalFunction = debounce( + () => { + console.log('Function executed'); + }, + 1000, + { signal } +); + +// 1초 안에 다시 호출되지 않으면, '실행됨'을 로깅해요 +debouncedWithSignalFunction(); + +// debounce 함수 호출을 취소해요 +controller.abort(); +``` diff --git a/docs/reference/function/debounce.md b/docs/reference/function/debounce.md index 994e064ff..5a63d6591 100644 --- a/docs/reference/function/debounce.md +++ b/docs/reference/function/debounce.md @@ -7,13 +7,19 @@ method to cancel any pending execution. ## Signature ```typescript -function debounce void>(func: F, debounceMs: number): F & { cancel: () => void }; +function debounce void>( + func: F, + debounceMs: number, + options?: DebounceOptions +): F & { cancel: () => void }; ``` ### Parameters - `func` (`F`): The function to debounce. -- `debounceMs`(`number`): The number of milliseconds to delay. +- `debounceMs` (`number`): The number of milliseconds to delay. +- `options` (`DebounceOptions`, optional): An options object. + - `signal` (`AbortSignal`, optional): An optional `AbortSignal` to cancel the debounced function. ### Returns @@ -21,6 +27,8 @@ function debounce void>(func: F, debounceMs: numbe ## Examples +### Basic Usage + ```typescript const debouncedFunction = debounce(() => { console.log('Function executed'); @@ -32,3 +40,23 @@ debouncedFunction(); // Will not log anything as the previous call is canceled debouncedFunction.cancel(); ``` + +### Using with an AbortSignal + +```typescript +const controller = new AbortController(); +const signal = controller.signal; +const debouncedWithSignalFunction = debounce( + () => { + console.log('Function executed'); + }, + 1000, + { signal } +); + +// Will log 'Function executed' after 1 second if not called again in that time +debouncedWithSignalFunction(); + +// Will cancel the debounced function call +controller.abort(); +``` diff --git a/package.json b/package.json index c5677e8d5..529797fd4 100644 --- a/package.json +++ b/package.json @@ -130,4 +130,4 @@ "lint": "eslint ./src --ext .ts", "format": "prettier --write ." } -} \ No newline at end of file +} diff --git a/src/function/debounce.spec.ts b/src/function/debounce.spec.ts index c9087f20a..889d09e13 100644 --- a/src/function/debounce.spec.ts +++ b/src/function/debounce.spec.ts @@ -94,4 +94,41 @@ describe('debounce', () => { expect(func).toHaveBeenCalledTimes(1); expect(func).toHaveBeenCalledWith('test', 123); }); + + it('should cancel the debounced function call if aborted via AbortSignal', async () => { + const func = vi.fn(); + const debounceMs = 50; + const controller = new AbortController(); + const signal = controller.signal; + const debouncedFunc = debounce(func, debounceMs, { signal }); + + debouncedFunc(); + controller.abort(); + + await delay(debounceMs); + + expect(func).not.toHaveBeenCalled(); + }); + + it('should not add multiple abort event listeners', async () => { + const func = vi.fn(); + const debounceMs = 100; + const controller = new AbortController(); + const signal = controller.signal; + const addEventListenerSpy = vi.spyOn(signal, 'addEventListener'); + + const debouncedFunc = debounce(func, debounceMs, { signal }); + + debouncedFunc(); + debouncedFunc(); + + await new Promise(resolve => setTimeout(resolve, 150)); + + expect(func).toHaveBeenCalledTimes(1); + + const listenerCount = addEventListenerSpy.mock.calls.filter(([event]) => event === 'abort').length; + expect(listenerCount).toBe(1); + + addEventListenerSpy.mockRestore(); + }); }); diff --git a/src/function/debounce.ts b/src/function/debounce.ts index e346b28c0..230e700cc 100644 --- a/src/function/debounce.ts +++ b/src/function/debounce.ts @@ -1,3 +1,7 @@ +interface DebounceOptions { + signal?: AbortSignal; +} + /** * 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` @@ -5,6 +9,8 @@ * * @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 {F & { cancel: () => void }} A new debounced function with a `cancel` method. * * @example @@ -17,25 +23,55 @@ * * // 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): F & { cancel: () => void } { +export function debounce void>( + func: F, + debounceMs: number, + { signal }: DebounceOptions = {} +): F & { cancel: () => void } { let timeoutId: number | NodeJS.Timeout | null = null; const debounced = function (...args: Parameters) { - if (timeoutId != null) { + if (timeoutId !== null) { clearTimeout(timeoutId); } + if (signal?.aborted) { + return; + } + timeoutId = setTimeout(() => { func(...args); + timeoutId = null; }, debounceMs); } as F & { cancel: () => void }; + const onAbort = function () { + debounced.cancel(); + }; + debounced.cancel = function () { - if (timeoutId != null) { + if (timeoutId !== null) { clearTimeout(timeoutId); + timeoutId = null; } + + signal?.removeEventListener('abort', onAbort); }; + signal?.addEventListener('abort', onAbort); + return debounced; }