From 0f9764f41efe0b975cef8229ec72fea1ed9df895 Mon Sep 17 00:00:00 2001 From: dudaodong Date: Fri, 9 Aug 2024 14:28:15 +0800 Subject: [PATCH] feat: add Throttle function --- docs/api/packages/function.md | 43 +++++++++++++ docs/en/api/packages/function.md | 43 +++++++++++++ function/function.go | 37 +++++++++++ function/function_example_test.go | 21 +++++++ function/function_test.go | 100 +++++++++++++++++++++++++----- 5 files changed, 228 insertions(+), 16 deletions(-) diff --git a/docs/api/packages/function.md b/docs/api/packages/function.md index 94bc899..554b27e 100644 --- a/docs/api/packages/function.md +++ b/docs/api/packages/function.md @@ -41,6 +41,7 @@ import ( - [Xnor](#Xnor) - [Nand](#Nand) - [AcceptIf](#AcceptIf) +- [Throttle](#Throttle)
@@ -740,4 +741,46 @@ func main() { // false } +``` + +### Throttle + +

创建一个函数的节流版本。返回的函数保证在每个时间间隔内最多只会被调用一次。

+ +函数签名: + +```go +func Throttle(fn func(), interval time.Duration) func() +``` + +示例: + +```go +package main + +import ( + "fmt" + "github.com/duke-git/lancet/v2/function" +) + +func main() { + callCount := 0 + + fn := func() { + callCount++ + } + + throttledFn := function.Throttle(fn, 1*time.Second) + + for i := 0; i < 5; i++ { + throttledFn() + } + + time.Sleep(1 * time.Second) + + fmt.Println(callCount) + + // Output: + // 1 +} ``` \ No newline at end of file diff --git a/docs/en/api/packages/function.md b/docs/en/api/packages/function.md index 1d0bb56..dabdb30 100644 --- a/docs/en/api/packages/function.md +++ b/docs/en/api/packages/function.md @@ -41,6 +41,8 @@ import ( - [Xnor](#Xnor) - [Nand](#Nand) - [AcceptIf](#AcceptIf) +- [Throttle](#Throttle) +
@@ -738,5 +740,46 @@ func main() { // 0 // false } +``` +### Throttle + +

Throttle creates a throttled version of the provided function. The returned function guarantees that it will only be invoked at most once per interval.

+ +Signature: + +```go +func Throttle(fn func(), interval time.Duration) func() +``` + +Example: + +```go +package main + +import ( + "fmt" + "github.com/duke-git/lancet/v2/function" +) + +func main() { + callCount := 0 + + fn := func() { + callCount++ + } + + throttledFn := function.Throttle(fn, 1*time.Second) + + for i := 0; i < 5; i++ { + throttledFn() + } + + time.Sleep(1 * time.Second) + + fmt.Println(callCount) + + // Output: + // 1 +} ``` \ No newline at end of file diff --git a/function/function.go b/function/function.go index 96d6cc0..221c905 100644 --- a/function/function.go +++ b/function/function.go @@ -125,6 +125,43 @@ func Debounce(fn func(), delay time.Duration) (debouncedFn func(), cancelFn func return debouncedFn, cancelFn } +// Throttle creates a throttled version of the provided function. +// The returned function guarantees that it will only be invoked at most once per interval. +// Play: todo +func Throttle(fn func(), interval time.Duration) func() { + var ( + timer *time.Timer + lastRun time.Time + mu sync.Mutex + ) + + return func() { + mu.Lock() + defer mu.Unlock() + + now := time.Now() + if now.Sub(lastRun) >= interval { + fn() + lastRun = now + if timer != nil { + timer.Stop() + timer = nil + } + } else if timer == nil { + delay := interval - now.Sub(lastRun) + + timer = time.AfterFunc(delay, func() { + mu.Lock() + defer mu.Unlock() + + fn() + lastRun = time.Now() + timer = nil + }) + } + } +} + // Schedule invoke function every duration time, util close the returned bool channel. // Play: https://go.dev/play/p/hbON-Xeyn5N func Schedule(d time.Duration, fn any, args ...any) chan bool { diff --git a/function/function_example_test.go b/function/function_example_test.go index 245d02a..4205949 100644 --- a/function/function_example_test.go +++ b/function/function_example_test.go @@ -200,3 +200,24 @@ func ExampleAcceptIf() { // 0 // false } + +func ExampleThrottle() { + callCount := 0 + + fn := func() { + callCount++ + } + + throttledFn := Throttle(fn, 1*time.Second) + + for i := 0; i < 5; i++ { + throttledFn() + } + + time.Sleep(1 * time.Second) + + fmt.Println(callCount) + + // Output: + // 1 +} diff --git a/function/function_test.go b/function/function_test.go index 99d7d6f..f334cff 100644 --- a/function/function_test.go +++ b/function/function_test.go @@ -130,11 +130,11 @@ func TestDebounce(t *testing.T) { t.Run("Single call", func(t *testing.T) { callCount := 0 - fn := func() { - callCount++ - } - debouncedFn, _ := Debounce(fn, 500*time.Millisecond) + debouncedFn, _ := Debounce(func() { + callCount++ + }, 500*time.Millisecond) + debouncedFn() time.Sleep(1 * time.Second) @@ -144,11 +144,10 @@ func TestDebounce(t *testing.T) { t.Run("Multiple calls within debounce interval", func(t *testing.T) { callCount := 0 - fn := func() { - callCount++ - } - debouncedFn, _ := Debounce(fn, 1*time.Second) + debouncedFn, _ := Debounce(func() { + callCount++ + }, 1*time.Second) for i := 0; i < 5; i++ { go func(index int) { @@ -164,11 +163,10 @@ func TestDebounce(t *testing.T) { t.Run("Immediate consecutive calls", func(t *testing.T) { callCount := 0 - fn := func() { - callCount++ - } - debouncedFn, _ := Debounce(fn, 500*time.Millisecond) + debouncedFn, _ := Debounce(func() { + callCount++ + }, 500*time.Millisecond) for i := 0; i < 10; i++ { debouncedFn() @@ -182,11 +180,10 @@ func TestDebounce(t *testing.T) { t.Run("Cancel calls", func(t *testing.T) { callCount := 0 - fn := func() { - callCount++ - } - debouncedFn, cancelFn := Debounce(fn, 500*time.Millisecond) + debouncedFn, cancelFn := Debounce(func() { + callCount++ + }, 500*time.Millisecond) debouncedFn() @@ -199,6 +196,77 @@ func TestDebounce(t *testing.T) { } +func TestThrottle(t *testing.T) { + assert := internal.NewAssert(t, "TestThrottle") + + t.Run("Single call", func(t *testing.T) { + callCount := 0 + + throttledFn := Throttle(func() { + callCount++ + }, 1*time.Second) + + throttledFn() + + time.Sleep(100 * time.Millisecond) + + assert.Equal(1, callCount) + }) + + t.Run("Multiple calls within throttle interval", func(t *testing.T) { + callCount := 0 + + throttledFn := Throttle(func() { + callCount++ + }, 1*time.Second) + + for i := 0; i < 5; i++ { + throttledFn() + } + + time.Sleep(1 * time.Second) + + assert.Equal(1, callCount) + }) + + t.Run("Mutiple calls space out throttle interval", func(t *testing.T) { + callCount := 0 + + throttledFn := Throttle(func() { + callCount++ + }, 500*time.Millisecond) + + throttledFn() + time.Sleep(600 * time.Millisecond) + + throttledFn() + time.Sleep(600 * time.Millisecond) + + throttledFn() + + time.Sleep(1 * time.Second) + + assert.Equal(3, callCount) + }) + + t.Run("Call function near the end of the interval", func(t *testing.T) { + callCount := 0 + + throttledFn := Throttle(func() { + callCount++ + }, 1*time.Second) + + throttledFn() + time.Sleep(900 * time.Millisecond) + + throttledFn() + time.Sleep(200 * time.Millisecond) + + assert.Equal(2, callCount) + }) + +} + func TestSchedule(t *testing.T) { // assert := internal.NewAssert(t, "TestSchedule")