mirror of
https://github.com/toss/es-toolkit.git
synced 2024-11-24 03:32:58 +03:00
feat(debounce): Support AbortSignal to cancel debounced functions for improved cancellation (#45)
* feat: Support AbortSignal to debounce for improved cancellation * refactor: cancel timeoutId & add strict inequality * fix: formatting in package.json * refactor: using optional chaining * fix: follow the fetch API's option * docs: modify debounce ko, en docs
This commit is contained in:
parent
fde86f7f4b
commit
a707c06f7f
@ -7,13 +7,19 @@ debounce된 함수는 또한 대기 중인 실행을 취소하는 `cancel` 메
|
||||
## 인터페이스
|
||||
|
||||
```typescript
|
||||
function debounce<F extends (...args: any[]) => void>(func: F, debounceMs: number): F & { cancel: () => void };
|
||||
function debounce<F extends (...args: any[]) => 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<F extends (...args: any[]) => 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();
|
||||
```
|
||||
|
@ -7,13 +7,19 @@ method to cancel any pending execution.
|
||||
## Signature
|
||||
|
||||
```typescript
|
||||
function debounce<F extends (...args: any[]) => void>(func: F, debounceMs: number): F & { cancel: () => void };
|
||||
function debounce<F extends (...args: any[]) => 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<F extends (...args: any[]) => 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();
|
||||
```
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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<F extends (...args: any[]) => void>(func: F, debounceMs: number): F & { cancel: () => void } {
|
||||
export function debounce<F extends (...args: any[]) => void>(
|
||||
func: F,
|
||||
debounceMs: number,
|
||||
{ signal }: DebounceOptions = {}
|
||||
): F & { cancel: () => void } {
|
||||
let timeoutId: number | NodeJS.Timeout | null = null;
|
||||
|
||||
const debounced = function (...args: Parameters<F>) {
|
||||
if (timeoutId != null) {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
func(...args);
|
||||
timeoutId = null;
|
||||
}, debounceMs);
|
||||
} as F & { cancel: () => void };
|
||||
|
||||
debounced.cancel = function () {
|
||||
if (timeoutId != null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
const onAbort = function () {
|
||||
debounced.cancel();
|
||||
};
|
||||
|
||||
debounced.cancel = function () {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
|
||||
signal?.removeEventListener('abort', onAbort);
|
||||
};
|
||||
|
||||
signal?.addEventListener('abort', onAbort);
|
||||
|
||||
return debounced;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user