diff --git a/packages/shared/utils/filters.ts b/packages/shared/utils/filters.ts index 96f51146253..6399f7e6242 100644 --- a/packages/shared/utils/filters.ts +++ b/packages/shared/utils/filters.ts @@ -1,4 +1,4 @@ -import { readonly, ref } from 'vue-demi' +import { isRef, readonly, ref } from 'vue-demi' import { toValue } from '../toValue' import { noop } from './is' import type { AnyFn, ArgumentsType, Awaited, MaybeRefOrGetter, Pausable, Promisify } from './types' @@ -114,6 +114,25 @@ export function debounceFilter(ms: MaybeRefOrGetter, options: DebounceFi return filter } +export interface ThrottleFilterOptions { + /** + * The maximum time allowed to be delayed before it's invoked. + */ + delay: MaybeRefOrGetter + /** + * Whether to invoke on the trailing edge of the timeout. + */ + trailing?: boolean + /** + * Whether to invoke on the leading edge of the timeout. + */ + leading?: boolean + /** + * Whether to reject the last call if it's been cancel. + */ + rejectOnCancel?: boolean +} + // TODO v11: refactor the params to object /** * Create an EventFilter that throttle the events @@ -123,13 +142,22 @@ export function debounceFilter(ms: MaybeRefOrGetter, options: DebounceFi * @param [leading] * @param [rejectOnCancel] */ -export function throttleFilter(ms: MaybeRefOrGetter, trailing = true, leading = true, rejectOnCancel = false) { +export function throttleFilter(ms: MaybeRefOrGetter, trailing?: boolean, leading?: boolean, rejectOnCancel?: boolean): EventFilter +export function throttleFilter(options: ThrottleFilterOptions): EventFilter +export function throttleFilter(...args: any[]) { let lastExec = 0 let timer: ReturnType | undefined let isLeading = true let lastRejector: AnyFn = noop let lastValue: any - + let ms: MaybeRefOrGetter + let trailing: boolean + let leading: boolean + let rejectOnCancel: boolean + if (!isRef(args[0]) && typeof args[0] === 'object') + ({ delay: ms, trailing = true, leading = true, rejectOnCancel = false } = args[0]) + else + [ms, trailing = true, leading = true, rejectOnCancel = false] = args const clear = () => { if (timer) { clearTimeout(timer) diff --git a/packages/shared/utils/index.test.ts b/packages/shared/utils/index.test.ts index a1e3cfb497a..2bab71e831e 100644 --- a/packages/shared/utils/index.test.ts +++ b/packages/shared/utils/index.test.ts @@ -137,7 +137,6 @@ describe('filters', () => { it('should throttle', () => { const throttledFilterSpy = vi.fn() const filter = createFilterWrapper(throttleFilter(1000), throttledFilterSpy) - setTimeout(filter, 500) setTimeout(filter, 500) setTimeout(filter, 500) @@ -353,3 +352,144 @@ describe('compatibility', () => { } }) }) + +describe('optionsFilters', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + it('optionsThrottleFilter should throttle', () => { + const throttledFilterSpy = vi.fn() + const filter = createFilterWrapper(throttleFilter({ + delay: 1000, + }), throttledFilterSpy) + setTimeout(filter, 500) + setTimeout(filter, 500) + setTimeout(filter, 500) + setTimeout(filter, 500) + + vi.runAllTimers() + + expect(throttledFilterSpy).toHaveBeenCalledTimes(2) + }) + + it('optionsThrottleFilter should throttle evenly', () => { + const debouncedFilterSpy = vi.fn() + + const filter = createFilterWrapper(throttleFilter({ + delay: 1000, + }), debouncedFilterSpy) + + setTimeout(() => filter(1), 500) + setTimeout(() => filter(2), 1000) + setTimeout(() => filter(3), 2000) + + vi.runAllTimers() + + expect(debouncedFilterSpy).toHaveBeenCalledTimes(3) + expect(debouncedFilterSpy).toHaveBeenCalledWith(1) + expect(debouncedFilterSpy).toHaveBeenCalledWith(2) + expect(debouncedFilterSpy).toHaveBeenCalledWith(3) + }) + + it('optionsThrottleFilter should throttle with ref', () => { + const debouncedFilterSpy = vi.fn() + const throttle = ref(0) + const filter = createFilterWrapper(throttleFilter(throttle), debouncedFilterSpy) + + filter() + throttle.value = 1000 + + setTimeout(filter, 300) + setTimeout(filter, 600) + setTimeout(filter, 900) + + vi.runAllTimers() + + expect(debouncedFilterSpy).toHaveBeenCalledTimes(2) + }) + + it('optionsThrottleFilter should not duplicate single event', () => { + const debouncedFilterSpy = vi.fn() + const filter = createFilterWrapper(throttleFilter({ + delay: 1000, + }), debouncedFilterSpy) + + setTimeout(filter, 500) + + vi.runAllTimers() + + expect(debouncedFilterSpy).toHaveBeenCalledTimes(1) + }) + + it('optionsThrottleFilter should get trailing value', () => { + const sumSpy = vi.fn((a: number, b: number) => a + b) + const throttledSum = createFilterWrapper( + throttleFilter({ + delay: 1000, + trailing: true, + }), + sumSpy, + ) + + let result = throttledSum(2, 3) + setTimeout(() => { + result = throttledSum(4, 5) + }, 600) + setTimeout(() => { + result = throttledSum(6, 7) + }, 900) + + vi.runAllTimers() + + expect(sumSpy).toHaveBeenCalledTimes(2) + expect(result).resolves.toBe(6 + 7) + + setTimeout(() => { + result = throttledSum(8, 9) + }, 1200) + setTimeout(() => { + result = throttledSum(10, 11) + }, 1800) + + vi.runAllTimers() + + expect(sumSpy).toHaveBeenCalledTimes(4) + expect(result).resolves.toBe(10 + 11) + }) + + it('optionsThrottleFilter should get leading value', () => { + const sumSpy = vi.fn((a: number, b: number) => a + b) + const throttledSum = createFilterWrapper( + throttleFilter({ + delay: 1000, + trailing: false, + }), + sumSpy, + ) + + let result = throttledSum(2, 3) + setTimeout(() => { + result = throttledSum(4, 5) + }, 600) + setTimeout(() => { + result = throttledSum(6, 7) + }, 900) + + vi.runAllTimers() + + expect(sumSpy).toHaveBeenCalledTimes(1) + expect(result).resolves.toBe(2 + 3) + + setTimeout(() => { + result = throttledSum(8, 9) + }, 1200) + setTimeout(() => { + result = throttledSum(10, 11) + }, 1800) + + vi.runAllTimers() + + expect(sumSpy).toHaveBeenCalledTimes(2) + expect(result).resolves.toBe(8 + 9) + }) +})