Skip to content

Commit 9576461

Browse files
authored
feat: add throttle function (#378)
* feat: add throttle function * feat: add i18n throttle spec description
1 parent 378a368 commit 9576461

File tree

7 files changed

+788
-2
lines changed

7 files changed

+788
-2
lines changed

src/Util/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import debounce from "./debounce";
22
import shuffle from "./shuffle";
3+
import throttle from "./throttle";
34

4-
export { debounce, shuffle };
5+
export { debounce, shuffle, throttle };

src/Util/throttle.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* `throttle` function creates a throttled version of a function that can only be invoked at most once per specified time period.
3+
* Unlike `debounce`, throttle ensures the function is called at regular intervals during continuous invocations, rather than waiting for a pause.
4+
* This is useful for rate-limiting expensive operations like scroll handlers, window resizing, or API calls.
5+
*
6+
* The throttled function supports `leading` and `trailing` edge execution:
7+
* - `leading`: Execute immediately on the first call
8+
* - `trailing`: Execute at the end of the throttle period with the latest arguments
9+
*
10+
* The throttled function includes a `.cancel()` method to cancel any pending trailing invocations.
11+
*
12+
* @example
13+
* Basic usage (default options - both leading and trailing):
14+
* ```ts
15+
* const throttled = throttle((val: number) => {
16+
* console.log('val', val);
17+
* }, 1000);
18+
*
19+
* throttled(1); // immediately logs: val 1
20+
* throttled(2); // ignored
21+
* throttled(3); // ignored
22+
* // After 1000ms, logs: val 3 (trailing call with latest args)
23+
* ```
24+
*
25+
* @example
26+
* Leading edge only (useful for preventing rapid repeated actions):
27+
* ```ts
28+
* const throttled = throttle((val: number) => {
29+
* console.log('val', val);
30+
* }, 1000, { leading: true, trailing: false });
31+
*
32+
* throttled(1); // immediately logs: val 1
33+
* throttled(2); // ignored
34+
* throttled(3); // ignored
35+
* // After 1000ms, nothing happens
36+
* ```
37+
*
38+
* @example
39+
* Trailing edge only (useful for batch processing):
40+
* ```ts
41+
* const throttled = throttle((val: number) => {
42+
* console.log('val', val);
43+
* }, 1000, { leading: false, trailing: true });
44+
*
45+
* throttled(1); // delayed
46+
* throttled(2); // delayed
47+
* throttled(3); // delayed
48+
* // After 1000ms, logs: val 3 (only the latest value)
49+
* ```
50+
*
51+
* @example
52+
* Canceling pending throttled invocations:
53+
* ```ts
54+
* const throttled = throttle((val: number) => {
55+
* console.log('val', val);
56+
* }, 1000);
57+
*
58+
* throttled(1); // immediately logs: val 1
59+
* throttled(2);
60+
* throttled(3);
61+
* throttled.cancel(); // cancels pending trailing invocation
62+
* // Nothing happens after 1000ms
63+
* ```
64+
*
65+
* @example
66+
* Real-world use case - throttling scroll events:
67+
* ```ts
68+
* const handleScroll = throttle(() => {
69+
* console.log('Scroll position:', window.scrollY);
70+
* }, 200);
71+
*
72+
* window.addEventListener('scroll', handleScroll);
73+
*
74+
* // The handler will fire immediately on first scroll,
75+
* // then at most once every 200ms during continuous scrolling,
76+
* // and once more after scrolling stops (trailing edge)
77+
* ```
78+
*/
79+
function throttle<T extends (...args: any[]) => void>(
80+
func: T,
81+
wait: number,
82+
options = { leading: true, trailing: true },
83+
): ((...args: Parameters<T>) => void) & { cancel: () => void } {
84+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
85+
let lastCallTime: number | undefined;
86+
let lastArgs: Parameters<T> | undefined;
87+
88+
const throttled = function (...args: Parameters<T>) {
89+
const currentTime = Date.now();
90+
91+
// First call handling
92+
if (lastCallTime === undefined) {
93+
if (options.leading) {
94+
func(...args);
95+
lastCallTime = currentTime;
96+
}
97+
lastArgs = args;
98+
99+
if (options.trailing) {
100+
timeoutId = setTimeout(() => {
101+
// Only call if there were subsequent calls and we haven't already called with these args
102+
if (
103+
lastArgs !== undefined &&
104+
(!options.leading || lastArgs !== args)
105+
) {
106+
func(...lastArgs);
107+
}
108+
lastCallTime = undefined;
109+
lastArgs = undefined;
110+
timeoutId = undefined;
111+
}, wait);
112+
}
113+
return;
114+
}
115+
116+
// Subsequent calls - store latest args but don't reset timer
117+
lastArgs = args;
118+
119+
if (options.trailing && !timeoutId) {
120+
const remainingTime = wait - (currentTime - lastCallTime);
121+
122+
timeoutId = setTimeout(() => {
123+
if (lastArgs !== undefined) {
124+
func(...lastArgs);
125+
lastCallTime = Date.now();
126+
}
127+
lastArgs = undefined;
128+
timeoutId = undefined;
129+
}, remainingTime);
130+
}
131+
};
132+
133+
throttled.cancel = () => {
134+
if (timeoutId) {
135+
clearTimeout(timeoutId);
136+
timeoutId = undefined;
137+
}
138+
lastCallTime = undefined;
139+
lastArgs = undefined;
140+
};
141+
142+
return throttled;
143+
}
144+
145+
export default throttle;

0 commit comments

Comments
 (0)