feat(debounce, throttle): Support edges for debounce & throttle

This commit is contained in:
Sojin Park 2024-09-18 16:11:14 +09:00
parent 3b983ff79c
commit aa89c90152
15 changed files with 542 additions and 141 deletions

View File

@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest';
import { getBundleSize } from './utils/getBundleSize';
describe('debounce bundle size', () => {
it('lodash-es', async () => {
const bundleSize = await getBundleSize('lodash-es', 'debounce');
expect(bundleSize).toMatchInlineSnapshot(`2873`);
});
it('es-toolkit', async () => {
const bundleSize = await getBundleSize('es-toolkit', 'debounce');
expect(bundleSize).toMatchInlineSnapshot(`535`);
});
it('es-toolkit/compat', async () => {
const bundleSize = await getBundleSize('es-toolkit/compat', 'debounce');
expect(bundleSize).toMatchInlineSnapshot(`995`);
});
});

View File

@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest';
import { getBundleSize } from './utils/getBundleSize';
describe('throttle bundle size', () => {
it('lodash-es', async () => {
const bundleSize = await getBundleSize('lodash-es', 'throttle');
expect(bundleSize).toMatchInlineSnapshot(`3111`);
});
it('es-toolkit', async () => {
const bundleSize = await getBundleSize('es-toolkit', 'throttle');
expect(bundleSize).toMatchInlineSnapshot(`763`);
});
it('es-toolkit/compat', async () => {
const bundleSize = await getBundleSize('es-toolkit/compat', 'throttle');
expect(bundleSize).toMatchInlineSnapshot(`1144`);
});
});

View File

@ -20,10 +20,18 @@ function debounce<F extends (...args: any[]) => void>(
- `debounceMs`(`number`): デバウンスで遅延させるミリ秒。
- `options` (`DebounceOptions`, オプション): オプションオブジェクト。
- `signal` (`AbortSignal`, オプション): デバウンスされた関数をキャンセルするためのオプションの `AbortSignal`
- `edges` (`Array<'leading' | 'trailing'>`, オプション): 元の関数をいつ実行するかを示す配列。デフォルトは `['trailing']` です。
- `'leading'` が含まれている場合、デバウンスされた関数が最初に呼び出されたときに即座に元の関数を実行します。
- `'trailing'` が含まれている場合、最後のデバウンスされた関数の呼び出しから `debounceMs` ミリ秒が経過した後に元の関数を実行します。
- `'leading'``'trailing'` の両方が含まれている場合、元の関数は遅延の開始時と終了時の両方で呼び出されます。ただし、両方の時点で呼び出されるためには、デバウンスされた関数が `debounceMs` ミリ秒の間に少なくとも2回呼び出される必要があります。デバウンスされた関数を1回呼び出して元の関数を2回呼び出すことはできません。
### 戻り値
(`((...args: Parameters<F>) => void) & { cancel: () => void }`): `cancel` メソッドを持つデバウンスされた関数。
(`((...args: Parameters<F>) => void) & { cancel: () => void; flush: () => void; schedule: () => void; }`): デバウンスされた関数。デバウンス動作を制御するための追加のメソッドを持っています。
- `cancel` (`() => void`): 保留中のデバウンス呼び出しをキャンセルします。
- `flush` (`() => void`): 保留中のデバウンス呼び出しを即座に実行します。
- `schedule` (`() => void`): デバウンス呼び出しが少なくとも `debounceMs` 後に実行されるようにスケジュールします。
## 例
@ -69,7 +77,6 @@ controller.abort();
- `leading`: デバウンスされた関数を最初に呼び出したときに即座に元の関数を実行するかどうかです。デフォルトは`false`です。
- `trailing`: 最後のデバウンスされた関数の呼び出しから`debounceMs`ミリ秒が経過した後に元の関数を実行するかどうかです。デフォルトは`true`です。
- `leading`と`trailing`の両方が`true`の場合、元の関数は遅延の開始時と終了時の両方で呼び出されます。ただし、両方の時点で呼び出されるためには、デバウンスされた関数が`debounceMs`ミリ秒の間に少なくとも2回呼び出される必要があります。デバウンスされた関数の1回の呼び出しで元の関数が2回呼び出されることはありません。
- `debounce`関数は`maxWait`オプションも受け取ります。

View File

@ -12,28 +12,61 @@ function throttle<F extends (...args: any[]) => void>(func: F, throttleMs: numbe
- `func` (`F`): スロットル化する関数。
- `throttleMs`(`number`): 実行をスロットル化するミリ秒。
- `options` (`ThrottleOptions`, オプション): オプションオブジェクト。
- `signal` (`AbortSignal`, オプション): スロットル化された関数をキャンセルするためのオプションの `AbortSignal`
- `edges` (`Array<'leading' | 'trailing'>`, オプション): 元の関数をいつ実行するかを示す配列。デフォルトは `['leading', 'trailing']` です。
- `'leading'` が含まれている場合、スロットル化された関数が最初に呼び出されたときに即座に元の関数を実行します。
- `'trailing'` が含まれている場合、最後のスロットル化された関数の呼び出しから `throttleMs` ミリ秒が経過した後に元の関数を実行します。
- `'leading'``'trailing'` の両方が含まれている場合、元の関数は遅延の開始時と終了時の両方で呼び出されます。ただし、両方の時点で呼び出されるためには、スロットル化された関数が `throttleMs` ミリ秒の間に少なくとも2回呼び出される必要があります。スロットル化された関数を1回呼び出して元の関数を2回呼び出すことはできません。
### 戻り値
(`(...args: Parameters<F>) => void`): 新しいスロットル化された関数。
(`((...args: Parameters<F>) => void) & { cancel: () => void; flush: () => void; }`): スロットル化された関数。スロットル動作を制御するための追加のメソッドを持っています。
- `cancel` (`() => void`): 保留中のスロットル呼び出しをキャンセルします。
- `flush` (`() => void`): 保留中のスロットル呼び出しを即座に実行します。
## 例
### 基本的な使用法
```typescript
const throttledFunction = throttle(() => {
console.log('呼び出し');
console.log('Function executed');
}, 1000);
// 'Function executed'を即座にロギングします
throttledFunction(); // 最初の呼び出しは関数を即座に呼び出します
// すぐに '呼び出し' をログに記録します
throttledFunction();
// 1秒後に 'Function executed'をロギングします
throttledFunction(); // 2回目の呼び出しは `throttleMs` 内で実行されましたが、'trailing' オプションのためスロットリング時間が終わると関数が呼び出されます
// スロットル時間内なので何もログに記録しません
throttledFunction();
// 1秒後
// 2秒後
setTimeout(() => {
throttledFunction(); // '呼び出し' をログに記録します
}, 1000);
throttledFunction(); // 'Function executed'を再度ロギングします
}, 2000); // スロットリング時間が経過したため元の関数が呼び出されます
```
### Windowイベントの取り扱い
```typescript
// スロットル化する関数
const logResize = () => {
console.log('ウィンドウがリサイズされました', new Date().toISOString());
};
// 関数をスロットル化します
const throttledResizeHandler = throttle(logResize, 1000);
// スロットル化された関数をリサイズイベントに登録します
window.addEventListener('resize', throttledResizeHandler);
// もはや必要ないイベントリスナーを解除します
const cleanup = () => {
window.removeEventListener('resize', throttledResizeHandler);
};
// 10秒後にイベントリスナーを解除します
setTimeout(cleanup, 10000);
```
## Lodashとの互換性
@ -44,7 +77,6 @@ setTimeout(() => {
- `leading`: スロットル化された関数を最初に呼び出したときに即座に元の関数を実行するかどうかです。デフォルトは`true`です。
- `trailing`: 最後のスロットル化された関数の呼び出しから`throttleMs`ミリ秒が経過した後に元の関数を実行するかどうかです。デフォルトは`true`です。
- `leading`と`trailing`の両方が`true`の場合、元の関数は遅延の開始時と終了時の両方で呼び出されます。ただし、両方の時点で呼び出されるためには、スロットル化された関数が`throttleMs`ミリ秒の間に少なくとも2回呼び出される必要があります。スロットル化された関数の1回の呼び出しで元の関数が2回呼び出されることはありません。
- デフォルトで、`throttleMs`オプションは`0`に設定されています。これは、関数の実行が次のティックまで遅延されることを意味します。

View File

@ -11,7 +11,11 @@ function debounce<F extends (...args: any[]) => void>(
func: F,
debounceMs: number,
options?: DebounceOptions
): ((...args: Parameters<F>) => void) & { cancel: () => void };
): ((...args: Parameters<F>) => void) & {
cancel: () => void;
flush: () => void;
schedule: () => void;
};
```
### 파라미터
@ -19,11 +23,19 @@ function debounce<F extends (...args: any[]) => void>(
- `func` (`F`): debounce된 함수를 만들 함수.
- `debounceMs`(`number`): debounce로 지연시킬 밀리초.
- `options` (`DebounceOptions`, optional): 옵션 객체.
- `signal` (`AbortSignal`, optional): debounce된 함수를 취소하기 위한 선택적 `AbortSignal`.
- `signal` (`AbortSignal`, optional): 디바운스된 함수를 취소하기 위한 선택적 `AbortSignal`.
- `edges` (`Array<'leading' | 'trailing'>`, optional): 원래 함수를 언제 실행할지 나타내는 배열. 기본값은 `['trailing']`이에요.
- `'leading'`이 포함되면, 디바운스된 함수를 처음으로 호출했을 때 즉시 원래 함수를 실행해요.
- `'trailing'`이 포함되면, 마지막 디바운스된 함수 호출로부터 `debounceMs` 밀리세컨드가 지나면 원래 함수를 실행해요.
- `'leading'``'trailing'`이 모두 포함된다면, 원래 함수는 실행을 지연하기 시작할 때와 끝날 때 모두 호출돼요. 그렇지만 양쪽 시점 모두에 호출되기 위해서는, 디바운스된 함수가 `debounceMs` 밀리세컨드 사이에 최소 2번은 호출되어야 해요. 디바운스된 함수를 한 번 호출해서 원래 함수를 두 번 호출할 수는 없기 때문이에요.
### 결괏값
### 반환
(`((...args: Parameters<F>) => void) & { cancel: () => void }`): `cancel` 메서드를 가지고 있는 debounce된 함수.
(`((...args: Parameters<F>) => void) & { cancel: () => void; flush: () => void; schedule: () => void; }`): 디바운스된 함수. 디바운스 동작을 제어하기 위한 추가적인 메서드를 가져요.
- `cancel` (`() => void`): 예정된 디바운스 호출을 취소해요.
- `flush` (`() => void`): 예정된 디바운스 호출을 즉시 실행해요.
- `schedule` (`() => void`): 디바운스 호출이 최소 `debounceMs` 이후에 실행되도록 스케줄링해요.
## 예시
@ -69,7 +81,6 @@ controller.abort();
- `leading`: 디바운스된 함수를 처음으로 호출했을 때 즉시 원래 함수를 실행할지 여부예요. 기본값은 `false`예요.
- `trailing`: 마지막 디바운스된 함수 호출로부터 `debounceMs` 밀리세컨드가 지나면 원래 함수를 실행할지 여부예요. 기본값은 `true`예요.
- `leading``trailing`가 모두 `true`라면, 원래 함수는 실행을 지연하기 시작할 때와 끝날 때 모두 호출돼요. 그렇지만 양쪽 시점 모두에 호출되기 위해서는, 디바운스된 함수가 `debounceMs` 밀리세컨드 사이에 최소 2번은 호출되어야 해요. 디바운스된 함수 호출 한 번이 원래 함수를 두 번 호출할 수는 없기 때문이에요.
- `debounce` 함수는 `maxWait` 옵션도 받아요.

View File

@ -5,35 +5,76 @@
## 인터페이스
```typescript
function throttle<F extends (...args: any[]) => void>(func: F, throttleMs: number): (...args: Parameters<F>) => void;
function throttle<F extends (...args: any[]) => void>(
func: F,
throttleMs: number,
options?: ThrottleOptions
): ((...args: Parameters<F>) => void) & {
cancel: () => void;
flush: () => void;
};
```
### 파라미터
- `func` (`F`): throttle할 함수.
- `throttleMs`(`number`): 실행을 throttle할 밀리초.
- `options` (`ThrottleOptions`, optional): 옵션 객체.
- `signal` (`AbortSignal`, optional): 스로틀링된 함수를 취소하기 위한 선택적 `AbortSignal`.
- `edges` (`Array<'leading' | 'trailing'>`, optional): 원래 함수를 언제 실행할지 나타내는 배열. 기본값은 `['leading', 'trailing']`이에요.
- `'leading'`이 포함되면, 스로틀링된 함수를 처음으로 호출했을 때 즉시 원래 함수를 실행해요.
- `'trailing'`이 포함되면, 마지막 스로틀링된 함수 호출로부터 `throttleMs` 밀리세컨드가 지나면 원래 함수를 실행해요.
- `'leading'``'trailing'`이 모두 포함된다면, 원래 함수는 실행을 지연하기 시작할 때와 끝날 때 모두 호출돼요. 그렇지만 양쪽 시점 모두에 호출되기 위해서는, 스로틀링된 함수가 `throttleMs` 밀리세컨드 사이에 최소 2번은 호출되어야 해요. 스로틀링된 함수를 한 번 호출해서 원래 함수를 두 번 호출할 수는 없기 때문이에요.
### 반환 값
(`(...args: Parameters<F>) => void`): 새로운 throttle된 함수.
(`((...args: Parameters<F>) => void) & { cancel: () => void; flush: () => void; }`): 스로틀링된 함수. 스로틀링 동작을 제어하기 위한 추가적인 메서드를 가져요.
- `cancel` (`() => void`): 예정된 스로틀링 호출을 취소해요.
- `flush` (`() => void`): 예정된 스로틀링 호출을 즉시 실행해요.
## 예시
### 기본 사용법
```typescript
const throttledFunction = throttle(() => {
console.log('호출');
console.log('Function executed');
}, 1000);
// 즉시 '호출'를 로깅해요
throttledFunction();
// 'Function executed'를 즉시 로깅해요
throttledFunction(); // 첫 번째 호출은 함수를 즉시 호출해요
// throttle 시간 내에 있으므로 아무 것도 로깅하지 않아
throttledFunction();
// 1초 이후에 'Function executed'를 로깅해
throttledFunction(); // 2번째 호출은 `throttleMs` 안에 실행되었지만, `'trailing'` 옵션 때문에 스로틀링 시간이 끝나면 함수가 호출돼요
// 1초 후
// 2초 뒤
setTimeout(() => {
throttledFunction(); // '호출'를 로깅해요
}, 1000);
throttledFunction(); // 'Function executed'를 다시 로깅해요
}, 2000); // 스로틀링 시간이 지났기 때문에 원래 함수가 호출돼요
```
### Window 이벤트 다루기
```typescript
// 스로틀링할 함수
const logResize = () => {
console.log('Window resized at', new Date().toISOString());
};
// 함수를 스로틀링해요
const throttledResizeHandler = throttle(logResize, 1000);
// 스로틀링된 함수를 Resize 이벤트에 등록해요
window.addEventListener('resize', throttledResizeHandler);
// 더 이상 필요 없는 이벤트 리스너를 해제해요
const cleanup = () => {
window.removeEventListener('resize', throttledResizeHandler);
};
// 10초 이후에 이벤트 리스너를 해제해요
setTimeout(cleanup, 10000);
```
## Lodash와의 호환성
@ -43,8 +84,7 @@ setTimeout(() => {
- `throttle` 함수는 `leading` and `trailing` 옵션을 받아요.
- `leading`: 스로틀링된 함수를 처음으로 호출했을 때 즉시 원래 함수를 실행할지 여부예요. 기본값은 `true`예요.
- `trailing`: 마지막 스로틀링된 함수 호출로부터 `debounceMs` 밀리세컨드가 지나면 원래 함수를 실행할지 여부예요. 기본값은 `true`예요.
- `leading``trailing`가 모두 `true`라면, 원래 함수는 실행을 제어하기 시작할 때와 끝날 때 모두 호출돼요. 그렇지만 양쪽 시점 모두에 호출되기 위해서는, 스로틀링된 함수가 `debounceMs` 밀리세컨드 사이에 최소 2번은 호출되어야 해요. 스로틀링된 함수 호출 한 번이 원래 함수를 두 번 호출할 수는 없기 때문이에요.
- `trailing`: 마지막 스로틀링된 함수 호출로부터 `throttleMs` 밀리세컨드가 지나면 원래 함수를 실행할지 여부예요. 기본값은 `true`예요.
- `throttleMs` 옵션의 기본값은 `0`이에요. 함수 호출이 다음 틱까지만 지연된다는 뜻이에요.
@ -69,7 +109,7 @@ const leadingFn = throttle(
leadingFn();
// trailing 옵션 예시
const trailingFn = debounce(
const trailingFn = throttle(
() => {
console.log('Trailing function executed');
},

View File

@ -11,7 +11,11 @@ function debounce<F extends (...args: any[]) => void>(
func: F,
debounceMs: number,
options?: DebounceOptions
): ((...args: Parameters<F>) => void) & { cancel: () => void };
): ((...args: Parameters<F>) => void) & {
cancel: () => void;
flush: () => void;
schedule: () => void;
};
```
### Parameters
@ -20,10 +24,18 @@ function debounce<F extends (...args: any[]) => void>(
- `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.
- `edges` (`Array<'leading' | 'trailing'>`, optional): An array specifying when the function should be called. Defaults to `['trailing']`.
- `'leading'`: If included, the function will be called immediately on the first call.
- `'trailing'`: If included, the function will be called after `debounceMs` milliseconds have passed since the last call.
- If both `'leading'` and `'trailing'` are included, the function will be called at both the start and end of the delay period. However, it must be called at least twice within `debounceMs` milliseconds for this to happen, as one debounced function call cannot trigger the function twice.
### Returns
(`((...args: Parameters<F>) => void) & { cancel: () => void }`): A new debounced function with a `cancel` method.
(`((...args: Parameters<F>) => void) & { cancel: () => void; flush: () => void; schedule: () => void; }`): A new debounced function with methods to manage execution.
- `cancel` (`() => void`): Cancels any pending execution of the debounced function.
- `flush` (`() => void`): Immediately invokes the debounced function, executing any pending calls.
- `schedule` (`() => void`): Schedules the execution of the debounced function after the specified debounce delay.
## Examples
@ -69,7 +81,6 @@ Import `debounce` from `es-toolkit/compat` for full compatibility with lodash.
- `leading`: If true, the function runs immediately on the first call. (defaults to `false`)
- `trailing`: If true, the function runs after `debounceMs` milliseconds have passed since the last call. (defaults to `true`)
- If both `leading` and `trailing` are true, the function runs at both the start and end of the delay period. However, it must be called at least twice within `debounceMs` milliseconds for this to happen, as one debounced function call cannot trigger the function twice.
- The `debounce` function also accepts a `maxWait` option:

View File

@ -7,35 +7,76 @@ within the wait time will not trigger the execution of the original function.
## Signature
```typescript
function throttle<F extends (...args: any[]) => void>(func: F, throttleMs: number): (...args: Parameters<F>) => void;
function throttle<F extends (...args: any[]) => void>(
func: F,
throttleMs: number,
options?: ThrottleOptions
): ((...args: Parameters<F>) => void) & {
cancel: () => void;
flush: () => void;
};
```
### Parameters
- `func` (`F`): The function to throttle.
- `throttleMs`(`number`): The number of milliseconds to throttle executions to.
- `options` (`ThrottleOptions`, optional): An options object.
- `signal` (`AbortSignal`, optional): An optional `AbortSignal` to cancel the throttled function.
- `edges` (`Array<'leading' | 'trailing'>`, optional): An array specifying when the function should be called. Defaults to `['leading', 'trailing']`.
- `'leading'`: If included, the function will be called immediately on the first call.
- `'trailing'`: If included, the function will be called after `throttleMs` milliseconds have passed since the last call.
- If both `'leading'` and `'trailing'` are included, the function will be called at both the start and end of the delay period. However, it must be called at least twice within `throttleMs` milliseconds for this to happen, as one throttled function call cannot trigger the function twice.
### Returns
(`(...args: Parameters<F>) => void`): A new throttled function.
(`((...args: Parameters<F>) => void) & { cancel: () => void; flush: () => void; }`): A new throttled function with methods to manage execution.
- `cancel` (`() => void`): Cancels any pending execution of the throttled function.
- `flush` (`() => void`): Immediately invokes the throttled function, executing any pending calls.
## Examples
### Basic usage
```typescript
const throttledFunction = throttle(() => {
console.log('Function executed');
}, 1000);
// Will log 'Function executed' immediately
throttledFunction();
throttledFunction(); // First call triggers execution immediately
// Will not log anything as it is within the throttle time
throttledFunction();
// Will log 'Function executed' after 1 second
throttledFunction(); // Second call is within the throttle period but triggers after 1 second due to trailing edge behavior
// After 1 second
// After 2 seconds
setTimeout(() => {
throttledFunction(); // Will log 'Function executed'
}, 1000);
throttledFunction(); // Will log 'Function executed' again
}, 2000); // This will log because the throttle period has passed
```
### Usage with window events
```typescript
// Example function to throttle
const logResize = () => {
console.log('Window resized at', new Date().toISOString());
};
// Create a throttled version of the logResize function
const throttledResizeHandler = throttle(logResize, 1000);
// Attach the throttled function to the window resize event
window.addEventListener('resize', throttledResizeHandler);
// Optional: Clean up the event listener when no longer needed
const cleanup = () => {
window.removeEventListener('resize', throttledResizeHandler);
};
// Example: Clean up after 10 seconds
setTimeout(cleanup, 10000);
```
## Compatibility with Lodash
@ -46,7 +87,6 @@ Import `throttle` from `es-toolkit/compat` for full compatibility with lodash.
- `leading`: If true, the function runs immediately on the first call. (defaults to `true`)
- `trailing`: If true, the function runs after `throttleMs` milliseconds have passed since the last call. (defaults to `true`)
- If both `leading` and `trailing` are true, the function runs at both the start and end of the delay period. However, it must be called at least twice within `debounceMs` milliseconds for this to happen, as one throttled function call cannot trigger the function twice.
- By default, the `throttleMs` option is set to `0`, meaning the function execution is only delayed until the next tick.

View File

@ -20,10 +20,18 @@ function debounce<F extends (...args: any[]) => void>(
- `debounceMs` (`number`): 延迟执行的毫秒数。
- `options` (`DebounceOptions`, 可选): 一个选项对象。
- `signal` (`AbortSignal`, 可选): 可选的 `AbortSignal` 对象,用于取消防抖函数的执行。
- `edges` (`Array<'leading' | 'trailing'>`, 可选): 一个数组,指定函数应在何时被调用。默认为 `['trailing']`
- `'leading'`: 如果包含,函数将在第一次调用时立即执行。
- `'trailing'`: 如果包含,函数将在距离上次调用 debounceMs 毫秒后执行。
- 如果同时包含 `'leading'``'trailing'`,函数将在延迟周期的开始和结束时都被调用。然而,必须在 `debounceMs` 毫秒内至少调用两次才能发生这种情况,因为一次防抖函数调用不能触发函数两次。
### 返回值
(`((...args: Parameters<F>) => void) & { cancel: () => void }`): 一个带有 `cancel` 方法的新的防抖函数。
(`((...args: Parameters<F>) => void) & { cancel: () => void; flush: () => void; schedule: () => void; }`): 一个新的防抖函数,具有管理执行的方法。
- `cancel` (`() => void`): 取消任何待定的防抖函数执行。
- `flush` (`() => void`): 立即调用防抖函数,执行任何待处理的调用。
- `schedule` (`() => void`): 在指定的防抖延迟后安排防抖函数的执行。
## 示例

View File

@ -7,35 +7,76 @@
## 签名
```typescript
function throttle<F extends (...args: any[]) => void>(func: F, throttleMs: number): (...args: Parameters<F>) => void;
function throttle<F extends (...args: any[]) => void>(
func: F,
throttleMs: number,
options?: ThrottleOptions
): ((...args: Parameters<F>) => void) & {
cancel: () => void;
flush: () => void;
};
```
### 参数
- `func` (`F`): 要节流的函数。
- `throttleMs` (`number`): 节流执行的毫秒数。
- `options` (`DebounceOptions`, 可选): 一个选项对象。
- `signal` (`AbortSignal`, 可选): 可选的 `AbortSignal` 对象,用于取消防抖函数的执行。
- `edges` (`Array<'leading' | 'trailing'>`, 可选): 一个数组,指定函数应在何时被调用。默认为 `['leading', 'trailing']`
- `'leading'`: 如果包含,函数将在第一次调用时立即执行。
- `'trailing'`: 如果包含,函数将在距离上次调用 debounceMs 毫秒后执行。
- 如果同时包含 `'leading'``'trailing'`,函数将在延迟周期的开始和结束时都被调用。然而,必须在 `debounceMs` 毫秒内至少调用两次才能发生这种情况,因为一次防抖函数调用不能触发函数两次。
### 返回值
(`(...args: Parameters<F>) => void`): 一个新的节流函数。
(`((...args: Parameters<F>) => void) & { cancel: () => void; flush: () => void; }`): 一个新的节流函数,具有管理执行的方法。
- `cancel` (`() => void`): 取消任何待定的节流函数执行。
- `flush` (`() => void`): 立即调用节流函数,执行任何待处理的调用。
## 示例
### 基本用法
```typescript
const throttledFunction = throttle(() => {
console.log('Function executed');
console.log('函数执行了');
}, 1000);
// 立即输出 'Function executed'
throttledFunction();
// 将立即记录 '函数执行了'
throttledFunction(); // 第一次调用立即触发执行
// 由于在节流时间内,不会输出任何内容
throttledFunction();
// 将在1秒后记录 '函数执行了'
throttledFunction(); // 第二次调用在节流周期内但由于尾部行为在1秒后触发
// 1秒后
// 2秒后
setTimeout(() => {
throttledFunction(); // 输出 'Function executed'
}, 1000);
throttledFunction(); // 将再次记录 '函数执行了'
}, 2000); // 这将记录,因为节流周期已经过去
```
### 窗口事件的使用示例
```typescript
// 要节流的示例函数
const logResize = () => {
console.log('窗口在', new Date().toISOString(), '时被调整大小');
};
// 创建 logResize 函数的节流版本
const throttledResizeHandler = throttle(logResize, 1000);
// 将节流函数附加到窗口调整大小事件
window.addEventListener('resize', throttledResizeHandler);
// 可选:在不再需要时清理事件监听器
const cleanup = () => {
window.removeEventListener('resize', throttledResizeHandler);
};
// 示例在10秒后清理
setTimeout(cleanup, 10000);
```
## 与 Lodash 的兼容性
@ -46,7 +87,6 @@ setTimeout(() => {
- `leading`:如果为 true则函数在第一次调用时立即运行。默认为 `true`
- `trailing`:如果为 true则函数在上次调用后的 `throttleMs` 毫秒后运行。(默认为 `true`
- 如果 `leading``trailing` 都为 true则函数会在延迟期的开始和结束时运行。然而必须在 `throttleMs` 毫秒内至少调用两次才能实现这一点,因为一次节流函数调用不能触发函数两次。
- 默认情况下,`throttleMs` 选项设置为 `0`,这意味着函数执行仅延迟到下一个 tick。

View File

@ -1,3 +1,5 @@
import { debounce as debounceToolkit } from '../../function/debounce.ts';
interface DebounceOptions {
/**
* An optional AbortSignal to cancel the debounced function.
@ -83,82 +85,56 @@ export function debounce<F extends (...args: any[]) => any>(
const { signal, leading = false, trailing = true, maxWait } = options;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let pendingAt: number | null = null;
const edges = Array(2);
if (leading) {
edges[0] = 'leading';
}
if (trailing) {
edges[1] = 'trailing';
}
let result: ReturnType<F> | undefined = undefined;
let pendingArgs: Parameters<F> | null = null;
let pendingAt: number | null = null;
const cancel = () => {
if (timeoutId != null) {
clearTimeout(timeoutId);
timeoutId = null;
}
pendingArgs = null;
pendingAt = null;
};
const flush = function (this: any) {
if (pendingArgs != null) {
result = func.apply(this, pendingArgs);
}
cancel();
return result;
};
const debounced = function (this: any, ...args: Parameters<F>): ReturnType<F> | undefined {
if (signal?.aborted) {
return;
}
const timer = () => {
timeoutId = null;
if (trailing && pendingArgs != null) {
result = func.apply(this, pendingArgs);
}
cancel();
};
pendingArgs = args;
const _debounced = debounceToolkit(
function (this: any, ...args: Parameters<F>) {
result = func.apply(this, args);
pendingAt = null;
},
debounceMs,
{ signal, edges }
);
const debounced = function (this: any, ...args: Parameters<F>) {
if (maxWait != null) {
if (pendingAt != null) {
if (pendingAt === null) {
pendingAt = Date.now();
} else {
if (Date.now() - pendingAt >= maxWait) {
result = func.apply(this, args);
pendingArgs = null;
pendingAt = Date.now();
_debounced.cancel();
_debounced.schedule();
return result;
}
} else {
pendingAt = Date.now();
}
}
const isFirstCall = timeoutId == null;
if (timeoutId != null) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(timer, debounceMs);
if (leading && isFirstCall) {
result = func.apply(this, args);
pendingArgs = null;
}
_debounced.apply(this, args);
return result;
};
const onAbort = cancel;
const flush = () => {
_debounced.flush();
return result;
};
debounced.cancel = cancel;
debounced.cancel = _debounced.cancel;
debounced.flush = flush;
signal?.addEventListener('abort', onAbort, { once: true });
return debounced;
}

View File

@ -53,6 +53,8 @@ export { spread } from './function/spread.ts';
export { attempt } from './function/attempt.ts';
export { rearg } from './function/rearg.ts';
export { curry } from './function/curry.ts';
export { debounce } from './function/debounce.ts';
export { throttle } from './function/throttle.ts';
export { get } from './object/get.ts';
export { set } from './object/set.ts';

View File

@ -1,8 +1,40 @@
interface DebounceTimer {
/**
* Checks if the timer is active.
* @returns {boolean} True if the timer is active, otherwise false.
*/
isActive: () => boolean;
/**
* Triggers the debounce timer.
* This method resets the timer and schedules the execution of the debounced function
* after the specified delay. If the timer is already active, it clears the existing timeout
* before setting a new one.
*/
trigger: () => void;
/**
* Cancels any pending execution of the debounced function.
* This method clears the active timer, ensuring that the function will not be called
* at the end of the debounce period. It also resets any stored context or arguments.
*/
cancel: () => void;
}
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'>;
}
/**
@ -43,37 +75,106 @@ interface DebounceOptions {
export function debounce<F extends (...args: any[]) => void>(
func: F,
debounceMs: number,
{ signal }: DebounceOptions = {}
): ((...args: Parameters<F>) => void) & { cancel: () => void } {
{ signal, edges }: DebounceOptions = {}
): ((...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 also cancels the current timer, ensuring that the function executes right away.
*/
flush: () => void;
} {
let pendingThis: any = undefined;
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();
}
cancel();
};
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const debounced = function (...args: Parameters<F>) {
if (timeoutId !== 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 = () => {
cancelTimer();
invoke();
};
const debounced = function (this: any, ...args: Parameters<F>) {
if (signal?.aborted) {
return;
}
timeoutId = setTimeout(() => {
func(...args);
timeoutId = null;
}, debounceMs);
};
pendingThis = this;
pendingArgs = args;
const onAbort = function () {
debounced.cancel();
};
const isFirstCall = timeoutId == null;
debounced.cancel = function () {
if (timeoutId !== null) {
clearTimeout(timeoutId);
timeoutId = null;
schedule();
if (leading && isFirstCall) {
invoke();
}
};
signal?.addEventListener('abort', onAbort, { once: true });
debounced.schedule = schedule;
debounced.cancel = cancel;
debounced.flush = flush;
signal?.addEventListener('abort', cancel, { once: true });
return debounced;
}

View File

@ -21,12 +21,16 @@ describe('throttle', () => {
throttledFunc();
await delay(throttleMs / 2);
throttledFunc();
throttledFunc();
expect(func).toHaveBeenCalledTimes(1);
await delay(throttleMs / 2 + 1);
expect(func).toHaveBeenCalledTimes(1);
throttledFunc();
expect(func).toHaveBeenCalledTimes(2);
});
@ -40,4 +44,64 @@ describe('throttle', () => {
expect(func).toHaveBeenCalledTimes(1);
expect(func).toHaveBeenCalledWith('test', 123);
});
it('should not trigger a trailing call when invoked once', async () => {
const func = vi.fn();
const throttleMs = 50;
const throttled = throttle(func, throttleMs);
throttled();
expect(func).toBeCalledTimes(1);
await delay(throttleMs + 1);
expect(func).toBeCalledTimes(1);
});
it('should trigger a trailing call as soon as possible', async () => {
const func = vi.fn();
const throttleMs = 50;
const throttled = throttle(func, throttleMs);
throttled();
throttled();
expect(func).toBeCalledTimes(1);
await delay(throttleMs + 1);
expect(func).toBeCalledTimes(2);
});
it('should be able to abort initial invocation', async () => {
const throttleMs = 50;
const func = vi.fn();
const controller = new AbortController();
controller.abort();
const throttled = throttle(func, throttleMs, { signal: controller.signal });
throttled();
throttled();
expect(func).toBeCalledTimes(0);
await delay(throttleMs + 1);
expect(func).toBeCalledTimes(0);
});
it('should be able to abort trailing edge invocation', async () => {
const throttleMs = 50;
const func = vi.fn();
const controller = new AbortController();
const throttled = throttle(func, throttleMs, { signal: controller.signal });
throttled();
throttled();
expect(func).toBeCalledTimes(1);
controller.abort();
await delay(throttleMs + 1);
expect(func).toBeCalledTimes(1);
});
});

View File

@ -1,3 +1,21 @@
import { debounce } from './debounce';
interface ThrottleOptions {
/**
* 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 ["leading", "trailing"]
*/
edges?: Array<'leading' | 'trailing'>;
}
/**
* Creates a throttled function that only invokes the provided function at most once
* per every `throttleMs` milliseconds. Subsequent calls to the throttled function
@ -26,18 +44,31 @@
*/
export function throttle<F extends (...args: any[]) => void>(
func: F,
throttleMs: number
): (...args: Parameters<F>) => void {
let lastCallTime: number | null;
throttleMs: number,
{ signal, edges = ['leading', 'trailing'] }: ThrottleOptions = {}
): ((...args: Parameters<F>) => void) & {
cancel: () => void;
flush: () => void;
} {
let pendingAt: number | null = null;
const throttledFunction = function (...args: Parameters<F>) {
const now = Date.now();
const debounced = debounce(func, throttleMs, { signal, edges });
if (lastCallTime == null || now - lastCallTime >= throttleMs) {
lastCallTime = now;
func(...args);
const throttled = function (...args: Parameters<F>) {
if (pendingAt == null) {
pendingAt = Date.now();
} else {
if (Date.now() - pendingAt >= throttleMs) {
debounced.cancel();
debounced(...args);
}
}
debounced(...args);
};
return throttledFunction;
throttled.cancel = debounced.cancel;
throttled.flush = debounced.flush;
return throttled;
}