mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
refactor(packages): move slider throttle from commit to change (#1219)
This commit is contained in:
@@ -99,3 +99,135 @@ describe('throttle', () => {
|
||||
expect(() => throttled.cancel()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('throttle with leading: true', () => {
|
||||
it('invokes immediately on the first call', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const callback = vi.fn();
|
||||
const throttled = throttle(callback, 100, { leading: true });
|
||||
|
||||
throttled('first');
|
||||
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
expect(callback).toHaveBeenCalledWith('first');
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('coalesces calls within the cooldown to a trailing invocation', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const callback = vi.fn();
|
||||
const throttled = throttle(callback, 100, { leading: true });
|
||||
|
||||
throttled('first');
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
|
||||
throttled('second');
|
||||
throttled('third');
|
||||
|
||||
// Still only the leading call so far.
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
// Trailing fires with latest args.
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
expect(callback).toHaveBeenLastCalledWith('third');
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('resets to leading after cooldown expires with no pending calls', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const callback = vi.fn();
|
||||
const throttled = throttle(callback, 100, { leading: true });
|
||||
|
||||
// First leading call.
|
||||
throttled('batch-1');
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
|
||||
// Let cooldown expire without any trailing calls.
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
|
||||
// Next call should be treated as a new leading invocation.
|
||||
throttled('batch-2');
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
expect(callback).toHaveBeenLastCalledWith('batch-2');
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('chains trailing into a new cooldown window', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const callback = vi.fn();
|
||||
const throttled = throttle(callback, 100, { leading: true });
|
||||
|
||||
// Leading call.
|
||||
throttled('a');
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
|
||||
// Pending trailing.
|
||||
throttled('b');
|
||||
|
||||
// Trailing fires at 100ms, starting a new cooldown.
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
expect(callback).toHaveBeenLastCalledWith('b');
|
||||
|
||||
// Another pending during the new cooldown.
|
||||
throttled('c');
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(callback).toHaveBeenCalledTimes(3);
|
||||
expect(callback).toHaveBeenLastCalledWith('c');
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('cancel prevents trailing invocation', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const callback = vi.fn();
|
||||
const throttled = throttle(callback, 100, { leading: true });
|
||||
|
||||
throttled('leading');
|
||||
throttled('pending');
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
|
||||
throttled.cancel();
|
||||
|
||||
vi.advanceTimersByTime(200);
|
||||
|
||||
// Only the leading call, no trailing.
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('accepts new leading call after cancel', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const callback = vi.fn();
|
||||
const throttled = throttle(callback, 100, { leading: true });
|
||||
|
||||
throttled('first');
|
||||
throttled.cancel();
|
||||
|
||||
throttled('after-cancel');
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
expect(callback).toHaveBeenLastCalledWith('after-cancel');
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('cancel is a no-op when no timer is pending', () => {
|
||||
const callback = vi.fn();
|
||||
const throttled = throttle(callback, 100, { leading: true });
|
||||
|
||||
expect(() => throttled.cancel()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,22 +5,69 @@ export interface Throttled<Args extends unknown[]> {
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export interface ThrottleOptions {
|
||||
/**
|
||||
* When `true`, the first call invokes `fn` immediately (leading edge) and
|
||||
* starts the cooldown window. Calls during cooldown are coalesced and fire
|
||||
* on the trailing edge. If no calls arrive during the window the next call
|
||||
* is treated as a fresh leading invocation.
|
||||
*/
|
||||
leading?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trailing-edge throttle: the first call schedules a timer; subsequent calls
|
||||
* within the window update the arguments. The function fires once per `ms`
|
||||
* window with the latest arguments.
|
||||
* Throttle: limits `fn` to at most once per `ms` window.
|
||||
*
|
||||
* - Default (no options): trailing-edge only — the first call schedules a
|
||||
* timer; subsequent calls within the window update the arguments. The
|
||||
* function fires once per window with the latest arguments.
|
||||
* - `{ leading: true }`: leading + trailing — the first call invokes
|
||||
* immediately and opens a cooldown window. Subsequent calls within the
|
||||
* window are coalesced to a single trailing-edge invocation.
|
||||
*/
|
||||
export function throttle<Args extends unknown[]>(fn: (...args: Args) => void, ms: number): Throttled<Args> {
|
||||
export function throttle<Args extends unknown[]>(
|
||||
fn: (...args: Args) => void,
|
||||
ms: number,
|
||||
options?: ThrottleOptions
|
||||
): Throttled<Args> {
|
||||
const leading = options?.leading ?? false;
|
||||
|
||||
let timerId: ReturnType<typeof setTimeout> | null = null;
|
||||
let latestArgs: Args;
|
||||
let hasPending = false;
|
||||
|
||||
function startCooldown(): void {
|
||||
timerId = setTimeout(() => {
|
||||
timerId = null;
|
||||
|
||||
if (hasPending) {
|
||||
hasPending = false;
|
||||
fn(...latestArgs);
|
||||
startCooldown();
|
||||
}
|
||||
}, ms);
|
||||
}
|
||||
|
||||
const throttled = (...args: Args): void => {
|
||||
latestArgs = args;
|
||||
if (timerId !== null) return;
|
||||
timerId = setTimeout(() => {
|
||||
timerId = null;
|
||||
fn(...latestArgs);
|
||||
}, ms);
|
||||
|
||||
if (leading) {
|
||||
if (timerId === null) {
|
||||
// No active window — fire immediately (leading edge).
|
||||
fn(...latestArgs);
|
||||
startCooldown();
|
||||
} else {
|
||||
// Inside cooldown — mark pending for trailing edge.
|
||||
hasPending = true;
|
||||
}
|
||||
} else {
|
||||
// Trailing-only (original behavior).
|
||||
if (timerId !== null) return;
|
||||
timerId = setTimeout(() => {
|
||||
timerId = null;
|
||||
fn(...latestArgs);
|
||||
}, ms);
|
||||
}
|
||||
};
|
||||
|
||||
throttled.cancel = (): void => {
|
||||
@@ -28,6 +75,7 @@ export function throttle<Args extends unknown[]>(fn: (...args: Args) => void, ms
|
||||
clearTimeout(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
hasPending = false;
|
||||
};
|
||||
|
||||
return throttled;
|
||||
|
||||
Reference in New Issue
Block a user