Skip to content

Commit

Permalink
feat(curry): add leading option for function throttle
Browse files Browse the repository at this point in the history
type(curry): extract the type of options object as type `ThrottleConfig`
doc(curry): modify description and examples of function `throttle`
test(curry): add test for `leading` option of function `throttle`
  • Loading branch information
LynnSha1ng committed Mar 21, 2024
1 parent 5e275be commit 1a90346
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 52 deletions.
5 changes: 3 additions & 2 deletions cdn/radash.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -560,15 +560,16 @@ const debounce = ({ delay, leading = false }, func) => {
debounced.flush = (...args) => func(...args);
return debounced;
};
const throttle = ({ interval }, func) => {
const throttle = ({ interval, leading = true }, func) => {
let ready = true;
let timer = void 0;
const throttled = (...args) => {
if (!ready)
return;
func(...args);
leading && func(...args);
ready = false;
timer = setTimeout(() => {
!leading && func(...args);
ready = true;
timer = void 0;
}, interval);
Expand Down
5 changes: 3 additions & 2 deletions cdn/radash.js
Original file line number Diff line number Diff line change
Expand Up @@ -563,15 +563,16 @@ var radash = (function (exports) {
debounced.flush = (...args) => func(...args);
return debounced;
};
const throttle = ({ interval }, func) => {
const throttle = ({ interval, leading = true }, func) => {
let ready = true;
let timer = void 0;
const throttled = (...args) => {
if (!ready)
return;
func(...args);
leading && func(...args);
ready = false;
timer = setTimeout(() => {
!leading && func(...args);
ready = true;
timer = void 0;
}, interval);
Expand Down
2 changes: 1 addition & 1 deletion cdn/radash.min.js

Large diffs are not rendered by default.

45 changes: 14 additions & 31 deletions docs/curry/debounce.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,41 +20,25 @@ const makeSearchRequest = event => {
api.movies.search(event.target.value)
}

input.addEventListener('change', debounce({ delay: 100 }, makeSearchRequest))
input.addEventListener(
'change',
debounce({ delay: 100, leading = true }, makeSearchRequest)
)
```

## Timing
## Timing & Leading

A visual of the debounce behavior when `delay` is `100`. The debounce function
returned by `debounce` can be called every millisecond but it will only call
the given callback after `delay` milliseconds have passed.
A visual of the debounce behavior when `delay` is `100` and `leading` is in different values.
The debounce function returned by `debounce` can be called every millisecond but it will only
call the given callback after `delay` milliseconds have passed. The `leading` option, `false`
by default, will call the source function immediately the first time the debounce function is
invoked when set to `true`.

```sh
Time: 0ms - - - - 100ms - - - - 200ms - - - - 300ms - - - - 400ms - - - -
debounce Invocations: x x x x - - - - - - - - x x x x x x x x x x - - - - - - - - - - - -
Source Invocations: - - - - - - - - - - x - - - - - - - - - - - - - - - - - x - - - - -
```

## Leading

The `leading` option, `false` by default, will call the source function immediately
the first time the debounce function is invoked when set to `true`.

```ts
// hide the header when scroll down and show it when scroll up.

const header = document.getElementById('page-header')
let lastY = 0

window.addEventListener(
'scroll',
debounce({ delay: 100, leading: true }, () => {
header.style.transform = `translateY(${
window.scrollY - lastY > 0 ? '-100%' : '0'
})`
lastY = window.scrollY
})
)
Time: 0ms - - - - 100ms - - - - 200ms - - - - 300ms - - - - 400ms - - - -
debounce Invocations: x x x x - - - - - - - - x x x x x x x x x x - - - - - - - - - - - -
Source Invocations(leading): x - - - - - - - - - x - - - - - - - - - - - - - - - - - x - - - - -
Source Invocations(not leading): - - - - - - - - - - x - - - - - - - - - - - - - - - - - x - - - - -
```

### Cancel
Expand Down Expand Up @@ -92,4 +76,3 @@ const debounced = debounce({ delay: 100 }, api.feed.refresh)

debounced.isPending()
```

33 changes: 19 additions & 14 deletions docs/curry/throttle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ group: 'Curry'
description: Create a throttled callback function
---




## Basic usage

Throttle accepts an options object with an `interval` and a source function to call
when invoked. When the returned function is invoked it will only call the source
function if the `interval` milliseconds of time has passed. Otherwise, it will ignore
the invocation.

the invocation. The `leading` option decides whether the source function is called on
the first invocation of the throttle function or not.

```ts
import { throttle } from 'radash'
Expand All @@ -22,24 +19,32 @@ const onMouseMove = () => {
rerender()
}

addEventListener('mousemove', throttle({ interval: 200 }, onMouseMove))
addEventListener(
'mousemove',
throttle({ interval: 200, leading = false }, onMouseMove)
)
```

## Timing
## Timing & Leading

A visual of the throttle behavior when `interval` is `200`. The throttle function
returned by `throttle` can be called every millisecond but it will only call
the given callback after `interval` milliseconds have passed.
A visual of the throttle behavior when `interval` is `200` and `leading` is in different
values. The throttle function returned by `throttle` can be called every millisecond but
it will only call the given callback after `interval` milliseconds have passed. The `leading`
option, `true` by default, will delay the execution cycle of source function by one interval
as a whole when set to `false`.

```sh
Time: 0ms - - - - 100ms - - - - 200ms - - - - 300ms - - - - 400ms - - - -
Throttle Invocations: x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x - - -
Source Invocations: x - - - - - - - - - - - - x - - - - - - - - - - - - - x - - - - - -
Time: 0ms - - - - 200ms - - - - 400ms - - - - 600ms - - - - 800ms - - - -
Throttle Invocations: x x x x x x x x x x x x x x x x x x x x x x x - - - - - - - - - - -
Source Invocations(leading): x - - - - - x - - - - - - x - - - - - - x - - - - - - - - - - - - -
Source Invocations(not leading): - - - - - - x - - - - - - x - - - - - - x - - - - - - x - - - - - -

```

### isThrottled

The function returned by `throttle` has a `isThrottled` method that when called will return if there is any active throttle.
The function returned by `throttle` has a `isThrottled` method that when called will return
if there is any active throttle.

```ts
const debounced = throttle({ interval: 200 }, onMouseMove)
Expand Down
19 changes: 17 additions & 2 deletions src/curry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,20 @@ export type ThrottledFunction<TArgs extends any[]> = {
isThrottled(): boolean
}

export type ThrottleConfig = {
/**
* The time in milliseconds to wait before calling the
* source function again.
*/
interval: number

/**
* whether the source function will be called on the first
* invocation of the debounce function. `true` by default
*/
leading?: boolean
}

/**
* Given a delay and a function returns a new function
* that will only call the source function after delay
Expand Down Expand Up @@ -563,17 +577,18 @@ export const debounce = <TArgs extends any[]>(
* have passed since the last invocation
*/
export const throttle = <TArgs extends any[]>(
{ interval }: { interval: number },
{ interval, leading = true }: ThrottleConfig,
func: (...args: TArgs) => any
) => {
let ready = true
let timer: NodeJS.Timeout | undefined = undefined

const throttled: ThrottledFunction<TArgs> = (...args: TArgs) => {
if (!ready) return
func(...args)
leading && func(...args)
ready = false
timer = setTimeout(() => {
!leading && func(...args)
ready = true
timer = undefined
}, interval)
Expand Down
20 changes: 20 additions & 0 deletions src/tests/curry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,26 @@ describe('curry module', () => {
assert.equal(calls, 2)
})

test('leading option is set to `false`', async () => {
let calls = 0
const func = _.throttle({ interval: 600, leading: false }, () => calls++)
func()
func()
func()
await _.sleep(550)
assert.equal(calls, 0)
func()
func()
func()
await _.sleep(60)
func()
func()
func()
assert.equal(calls, 1)
await _.sleep(610)
assert.equal(calls, 2)
})

test('returns if the throttle is active', async () => {
const results = []
const func = _.throttle({ interval: 600 }, () => {})
Expand Down

0 comments on commit 1a90346

Please sign in to comment.