【问题标题】:C# event debounceC# 事件去抖动
【发布时间】:2015-04-12 21:04:13
【问题描述】:

我正在收听硬件事件消息,但我需要对其进行去抖动以避免太多查询。

这是一个发送机器状态的硬件事件,我必须将其存储在数据库中以用于统计目的,有时它的状态会经常变化(闪烁?)。在这种情况下,我只想存储一个“稳定”状态,我想通过在将状态存储到数据库之前等待 1-2 秒来实现它。

这是我的代码:

private MachineClass connect()
{
    try
    {
        MachineClass rpc = new MachineClass();
        rpc.RxVARxH += eventRxVARxH;
        return rpc;
    }
    catch (Exception e1)
    {
        log.Error(e1.Message);
        return null;
    }
}

private void eventRxVARxH(MachineClass Machine)
{
    log.Debug("Event fired");
}

我将此行为称为“去抖动”:等待几次以真正完成它的工作:如果在去抖动时间内再次触发相同的事件,我必须关闭第一个请求并开始等待去抖动时间以完成第二个事件。

管理它的最佳选择是什么?只是一个一次性计时器?

要解释“去抖动”功能,请查看关键事件的这个 javascript 实现: http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/

【问题讨论】:

  • 用秒表测量已经过去的时间。
  • “我必须拒绝第一个请求”——这听起来很成问题。是否有特定原因导致第一个请求无法继续,而随后的请求被忽略?
  • 这是一个发送机器状态的硬件事件,我必须将其存储在数据库中以用于统计目的,有时它的状态会经常变化(闪烁?),在这种情况下我想只存储“稳定”状态,我想实现它只需等待 1-2 秒,然后再将状态存储到数据库。也许我可以用计时器来做?如果状态更改太接近,我会等待 1-2 秒来触发重置它的查询。
  • @Damien_The_Unbeliever 这不是一个奇怪的要求,也不是微不足道的。它不能只用一个简单的计时器来处理。不过,响应式扩展确实涵盖了这种情况。
  • 一个非常类似的问题是等待 FileSystemWatcher 停止报告更改,例如。在复制大文件时。您会收到很多更改事件,但真的想在 last 事件之后稍等片刻,然后再尝试访问修改后的文件。

标签: c# events debouncing


【解决方案1】:

只需记住最新的'命中:

DateTime latestHit = DatetIme.MinValue;

private void eventRxVARxH(MachineClass Machine)
{
    log.Debug("Event fired");
    if(latestHit - DateTime.Now < TimeSpan.FromXYZ() // too fast
    {
        // ignore second hit, too fast
        return;
    }
    latestHit = DateTime.Now;
    // it was slow enough, do processing
    ...
}

如果在最后一个事件之后有足够的时间,这将允许第二个事件。

请注意:不可能(以简单的方式)处理一系列快速事件中的 last 事件,因为您永远不知道哪个是 last...

...除非您准备好处理很久以前的爆发的最后一个事件。然后你必须记住最后一个事件并在下一个事件足够慢时记录它:

DateTime latestHit = DatetIme.MinValue;
Machine historicEvent;

private void eventRxVARxH(MachineClass Machine)
{
    log.Debug("Event fired");

    if(latestHit - DateTime.Now < TimeSpan.FromXYZ() // too fast
    {
        // ignore second hit, too fast
        historicEvent = Machine; // or some property
        return;
    }
    latestHit = DateTime.Now;
    // it was slow enough, do processing
    ...
    // process historicEvent
    ...
    historicEvent = Machine; 
}

【讨论】:

  • 我想要相反:我想考虑最后一个而不是第一个。
  • 我可能不知道最后一个是什么,但我可以等着用计时器来做这个事件,如果 1-2 秒内没有任何事情发生,我不会中止计时器并让回调完成它的工作.否则,我会触发另一个事件,我必须重置计时器并再次开始等待 1-2 秒。
  • 如果没有计时器,只有在最后一个事件 XYZ ms 之后,这才有效
【解决方案2】:

这不是一个从头开始编写代码的简单请求,因为存在一些细微差别。一个类似的场景是监视 FileSystemWatcher 并等待在大副本之后安静下来,然后再尝试打开修改后的文件。

.NET 4.5 中的响应式扩展正是为了处理这些场景而创建的。您可以通过ThrottleBufferWindowSample 等方法轻松使用它们来提供此类功能。您将事件发布到Subject,对其应用窗口函数之一,例如,仅当 X 秒或 Y 事件没有活动时才获得通知,然后订阅通知。

Subject<MyEventData> _mySubject=new Subject<MyEventData>();
....
var eventSequenc=mySubject.Throttle(TimeSpan.FromSeconds(1))
                          .Subscribe(events=>MySubscriptionMethod(events));

只有当窗口中没有其他事件时,Throttle 才会返回滑动窗口中的最后一个事件。任何事件都会重置窗口。

你可以找到一个很好的时移函数概览here

当你的代码收到事件后,你只需要用 OnNext 将其发布到 Subject 即可:

_mySubject.OnNext(MyEventData);

如果您的硬件事件表现为典型的 .NET 事件,您可以使用 Observable.FromEventPattern 绕过主题和手动发布,如 here 所示:

var mySequence = Observable.FromEventPattern<MyEventData>(
    h => _myDevice.MyEvent += h,
    h => _myDevice.MyEvent -= h);  
_mySequence.Throttle(TimeSpan.FromSeconds(1))
           .Subscribe(events=>MySubscriptionMethod(events));

您还可以从 Tasks 创建 observables,将事件序列与 LINQ 运算符组合以请求,例如:使用 Zip 对不同的硬件事件进行请求,使用另一个事件源来绑定 Throttle/Buffer 等,添加延迟等等。

Reactive Extensions 以NuGet package 的形式提供,因此很容易将它们添加到您的项目中。

Stephen Cleary 的书“Concurrency in C# Cookbook”是一个非常关于 Reactive Extensions 的优秀资源,并解释了如何使用它以及它如何与 . NET 之类的任务、事件等。

Introduction to Rx 是一个优秀的系列文章(我从那里复制了示例),并附有几个示例。

更新

使用您的具体示例,您可以执行以下操作:

IObservable<MachineClass> _myObservable;

private MachineClass connect()
{

    MachineClass rpc = new MachineClass();
   _myObservable=Observable
                 .FromEventPattern<MachineClass>(
                            h=> rpc.RxVARxH += h,
                            h=> rpc.RxVARxH -= h)
                 .Throttle(TimeSpan.FromSeconds(1));
   _myObservable.Subscribe(machine=>eventRxVARxH(machine));
    return rpc;
}

这当然可以大大改进 - observable 和订阅都需要在某个时候处理。此代码假定您只控制一个设备。如果您有很多设备,您可以在类中创建 observable,以便每个 MachineClass 公开和处置自己的 observable。

【讨论】:

  • 这似乎是答案!谢谢...但是实现起来并不容易,我无法理解如何将您的示例应用到我的代码中。
  • 谢谢,我得到了我之前错过的东西!
【解决方案3】:

我已经使用它来消除事件并取得了一些成功:

public static Action<T> Debounce<T>(this Action<T> func, int milliseconds = 300)
{
    var last = 0;
    return arg =>
    {
        var current = Interlocked.Increment(ref last);
        Task.Delay(milliseconds).ContinueWith(task =>
        {
            if (current == last) func(arg);
            task.Dispose();
        });
    };
}

用法

Action<int> a = (arg) =>
{
    // This was successfully debounced...
    Console.WriteLine(arg);
};
var debouncedWrapper = a.Debounce<int>();

while (true)
{
    var rndVal = rnd.Next(400);
    Thread.Sleep(rndVal);
    debouncedWrapper(rndVal);
}

它可能不如 RX 中的强大,但它易于理解和使用。

跟进 2020-02-03

使用取消令牌修改@collie的解决方案如下

public static Action<T> Debounce<T>(this Action<T> func, int milliseconds = 300)
{
    CancellationTokenSource? cancelTokenSource = null;

    return arg =>
    {
        cancelTokenSource?.Cancel();
        cancelTokenSource = new CancellationTokenSource();

        Task.Delay(milliseconds, cancelTokenSource.Token)
            .ContinueWith(t =>
            {
                if (t.IsCompletedSuccessfully)
                {
                    func(arg);
                }
            }, TaskScheduler.Default);
    };
}

注意事项:

  • 调用Cancel 足以处理CTS
  • 成功完成的 CTS 直到下一次调用才会取消/处置
  • 正如@collie 所指出的,任务会被释放,因此无需在任务上调用Dispose

我以前没有使用过取消令牌,可能没有正确使用它们。

【讨论】:

  • 你是如何使用它的?
  • Slick,我花了一些时间才注意到你如何取消已经“运行”的动作:-)。但是这种方法有一个问题,您没有对去抖动器的反馈/控制,因此您无法控制所有操作何时完成。这很麻烦,例如当您处置主对象时,您没有意识到处置后将执行去抖动动作。
  • 请参阅stackoverflow.com/a/59296962/545233,了解使用取消令牌清理未使用任务的版本。
  • IsCompletedSuccessfully 仅在 .NET Core 中可用。您可以改用!t.IsCanceled 以使代码也可以在.NET Framework 中运行。
【解决方案4】:

Panagiotis 的回答当然是正确的,但是我想举一个更简单的例子,因为我花了一段时间来整理如何让它工作。我的场景是用户在搜索框中输入内容,当用户输入内容时,我们希望调用 api 调用以返回搜索建议,因此我们想要对 api 调用进行去抖动处理,这样他们就不会在每次输入字符时都进行一次调用。

我正在使用 Xamarin.Android,但这应该适用于任何 C# 方案...

private Subject<string> typingSubject = new Subject<string> ();
private IDisposable typingEventSequence;

private void Init () {
            var searchText = layoutView.FindViewById<EditText> (Resource.Id.search_text);
            searchText.TextChanged += SearchTextChanged;
            typingEventSequence = typingSubject.Throttle (TimeSpan.FromSeconds (1))
                .Subscribe (query => suggestionsAdapter.Get (query));
}

private void SearchTextChanged (object sender, TextChangedEventArgs e) {
            var searchText = layoutView.FindViewById<EditText> (Resource.Id.search_text);
            typingSubject.OnNext (searchText.Text.Trim ());
        }

public override void OnDestroy () {
            if (typingEventSequence != null)
                typingEventSequence.Dispose ();
            base.OnDestroy ();
        }

当你第一次初始化屏幕/类时,你创建你的事件来监听用户输入(SearchTextChanged),然后还设置一个限制订阅,它与“typingSubject”相关联。

接下来,在 SearchTextChanged 事件中,您可以调用 typingSubject.OnNext 并传入搜索框的文本。在去抖周期(1 秒)之后,它会调用订阅的事件(在我们的例子中是suggestionsAdapter.Get。)

最后,当屏幕关闭时,请务必处理订阅!

【讨论】:

    【解决方案5】:

    RX 可能是最简单的选择,尤其是当您已经在应用程序中使用它时。但如果没有,添加它可能有点矫枉过正。

    对于基于 UI 的应用程序(如 WPF),我使用以下使用 DispatcherTimer 的类:

    public class DebounceDispatcher
    {
        private DispatcherTimer timer;
        private DateTime timerStarted { get; set; } = DateTime.UtcNow.AddYears(-1);
    
        public void Debounce(int interval, Action<object> action,
            object param = null,
            DispatcherPriority priority = DispatcherPriority.ApplicationIdle,
            Dispatcher disp = null)
        {
            // kill pending timer and pending ticks
            timer?.Stop();
            timer = null;
    
            if (disp == null)
                disp = Dispatcher.CurrentDispatcher;
    
            // timer is recreated for each event and effectively
            // resets the timeout. Action only fires after timeout has fully
            // elapsed without other events firing in between
            timer = new DispatcherTimer(TimeSpan.FromMilliseconds(interval), priority, (s, e) =>
            {
                if (timer == null)
                    return;
    
                timer?.Stop();
                timer = null;
                action.Invoke(param);
            }, disp);
    
            timer.Start();
        }
    }
    

    使用它:

    private DebounceDispatcher debounceTimer = new DebounceDispatcher();
    
    private void TextSearchText_KeyUp(object sender, KeyEventArgs e)
    {
        debounceTimer.Debounce(500, parm =>
        {
            Model.AppModel.Window.ShowStatus("Searching topics...");
            Model.TopicsFilter = TextSearchText.Text;
            Model.AppModel.Window.ShowStatus();
        });
    }
    

    现在仅在键盘空闲 200 毫秒后才处理键事件 - 任何以前的待处理事件都将被丢弃。

    还有一个 Throttle 方法,它总是在给定的时间间隔后触发事件:

        public void Throttle(int interval, Action<object> action,
            object param = null,
            DispatcherPriority priority = DispatcherPriority.ApplicationIdle,
            Dispatcher disp = null)
        {
            // kill pending timer and pending ticks
            timer?.Stop();
            timer = null;
    
            if (disp == null)
                disp = Dispatcher.CurrentDispatcher;
    
            var curTime = DateTime.UtcNow;
    
            // if timeout is not up yet - adjust timeout to fire 
            // with potentially new Action parameters           
            if (curTime.Subtract(timerStarted).TotalMilliseconds < interval)
                interval = (int) curTime.Subtract(timerStarted).TotalMilliseconds;
    
            timer = new DispatcherTimer(TimeSpan.FromMilliseconds(interval), priority, (s, e) =>
            {
                if (timer == null)
                    return;
    
                timer?.Stop();
                timer = null;
                action.Invoke(param);
            }, disp);
    
            timer.Start();
            timerStarted = curTime;            
        }
    

    【讨论】:

      【解决方案6】:

      最近我正在对一个针对旧版本 .NET 框架 (v3.5) 的应用程序进行一些维护。

      我不能使用响应式扩展或任务并行库,但我需要一种很好、干净、一致的去抖动事件的方法。这是我想出的:

      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Threading;
      
      namespace MyApplication
      {
          public class Debouncer : IDisposable
          {
              readonly TimeSpan _ts;
              readonly Action _action;
              readonly HashSet<ManualResetEvent> _resets = new HashSet<ManualResetEvent>();
              readonly object _mutex = new object();
      
              public Debouncer(TimeSpan timespan, Action action)
              {
                  _ts = timespan;
                  _action = action;
              }
      
              public void Invoke()
              {
                  var thisReset = new ManualResetEvent(false);
      
                  lock (_mutex)
                  {
                      while (_resets.Count > 0)
                      {
                          var otherReset = _resets.First();
                          _resets.Remove(otherReset);
                          otherReset.Set();
                      }
      
                      _resets.Add(thisReset);
                  }
      
                  ThreadPool.QueueUserWorkItem(_ =>
                  {
                      try
                      {
                          if (!thisReset.WaitOne(_ts))
                          {
                              _action();
                          }
                      }
                      finally
                      {
                          lock (_mutex)
                          {
                              using (thisReset)
                                  _resets.Remove(thisReset);
                          }
                      }
                  });
              }
      
              public void Dispose()
              {
                  lock (_mutex)
                  {
                      while (_resets.Count > 0)
                      {
                          var reset = _resets.First();
                          _resets.Remove(reset);
                          reset.Set();
                      }
                  }
              }
          }
      }
      

      以下是在具有搜索文本框的 Windows 窗体中使用它的示例:

      public partial class Example : Form 
      {
          private readonly Debouncer _searchDebouncer;
      
          public Example()
          {
              InitializeComponent();
              _searchDebouncer = new Debouncer(TimeSpan.FromSeconds(.75), Search);
              txtSearchText.TextChanged += txtSearchText_TextChanged;
          }
      
          private void txtSearchText_TextChanged(object sender, EventArgs e)
          {
              _searchDebouncer.Invoke();
          }
      
          private void Search()
          {
              if (InvokeRequired)
              {
                  Invoke((Action)Search);
                  return;
              }
      
              if (!string.IsNullOrEmpty(txtSearchText.Text))
              {
                  // Search here
              }
          }
      }
      

      【讨论】:

        【解决方案7】:

        我遇到了这个问题。我在这里尝试了每个答案,并且由于我在 Xamarin 通用应用程序中,我似乎缺少每个答案中所需的某些内容,并且我不想添加任何更多的包或库。我的解决方案完全按照我的预期工作,而且我没有遇到任何问题。希望它可以帮助某人。

        using System;
        using System.Collections.Generic;
        using System.Threading;
        using System.Threading.Tasks;
        
        namespace OrderScanner.Models
        {
            class Debouncer
            {
                private List<CancellationTokenSource> StepperCancelTokens = new List<CancellationTokenSource>();
                private int MillisecondsToWait;
                private readonly object _lockThis = new object(); // Use a locking object to prevent the debouncer to trigger again while the func is still running
        
                public Debouncer(int millisecondsToWait = 300)
                {
                    this.MillisecondsToWait = millisecondsToWait;
                }
        
                public void Debouce(Action func)
                {
                    CancelAllStepperTokens(); // Cancel all api requests;
                    var newTokenSrc = new CancellationTokenSource();
                    lock (_lockThis)
                    {
                        StepperCancelTokens.Add(newTokenSrc);
                    }
                    Task.Delay(MillisecondsToWait, newTokenSrc.Token).ContinueWith(task => // Create new request
                    {
                        if (!newTokenSrc.IsCancellationRequested) // if it hasn't been cancelled
                        {
                            CancelAllStepperTokens(); // Cancel any that remain (there shouldn't be any)
                            StepperCancelTokens = new List<CancellationTokenSource>(); // set to new list
                            lock (_lockThis)
                            {
                                func(); // run
                            }
                        }
                    }, TaskScheduler.FromCurrentSynchronizationContext());
                }
        
                private void CancelAllStepperTokens()
                {
                    foreach (var token in StepperCancelTokens)
                    {
                        if (!token.IsCancellationRequested)
                        {
                            token.Cancel();
                        }
                    }
                }
            }
        }
        

        就是这么称呼的……

        private Debouncer StepperDeboucer = new Debouncer(1000); // one second
        
        StepperDeboucer.Debouce(() => { WhateverMethod(args) });
        

        对于机器每秒可以发送数百个请求的任何事情,我不建议这样做,但对于用户输入,它工作得很好。我在一个 android/IOS 应用程序的步进器上使用它,该应用程序在 step 上调用 api。

        【讨论】:

        • 我不明白这是如何工作的。似乎如果Debounce get 的调用频率高于MillisecondsToWait milis,则代码将永远不会执行。我错过了什么吗?
        • 你试过了吗?在我的实现中完美运行。只需将你的去抖时间设置为 2000 左右,然后调试看看它是如何工作的。
        • 哦,当然你没有得到输出,这是一个去抖动,而不是一个油门。 Debounce 会等待输入事件在定义的时间内停止,然后再运行该函数。如果你想要一个节流阀(在定义的时间内运行这么多次),这不是你想要的解决方案。
        • 好吧,这更有意义。感谢您的澄清。我完全误解了代码的目标。
        • 如果您使用它与来自 UI 线程的事件进行交互(就像您通常那样),值得将 TaskScheduler.FromCurrentSynchronizationContext() 添加到 .ContinueWith 调用的第二个参数。否则,您将不得不再次调用以将线程返回到 UI,这会破坏性能。见:stackoverflow.com/questions/4331262/…
        【解决方案8】:

        我在我的类定义中提出了这个。

        如果在这段时间(示例中为 3 秒)内没有任何操作,我想立即运行我的操作。

        如果最近三秒内发生了什么事,我想发送那段时间内发生的最后一件事。

            private Task _debounceTask = Task.CompletedTask;
            private volatile Action _debounceAction;
        
            /// <summary>
            /// Debounces anything passed through this 
            /// function to happen at most every three seconds
            /// </summary>
            /// <param name="act">An action to run</param>
            private async void DebounceAction(Action act)
            {
                _debounceAction = act;
                await _debounceTask;
        
                if (_debounceAction == act)
                {
                    _debounceTask = Task.Delay(3000);
                    act();
                }
            }
        

        所以,如果我将时钟细分为每一刻钟

          TIME:  1e&a2e&a3e&a4e&a5e&a6e&a7e&a8e&a9e&a0e&a
          EVENT:  A         B    C   D  E              F  
        OBSERVED: A           B           E            F
        

        请注意,不会尝试提前取消任务,因此操作可能会堆积 3 秒,然后最终可用于垃圾回收。

        【讨论】:

        • 我尝试了这个,因为我喜欢代码的干净程度,但这会在关闭应用程序时导致性能问题,知道为什么会这样吗?除了添加此代码之外,没有其他任何更改。
        • 如果关闭请求的时间有延迟,比如 3 秒,您可以将相同的取消令牌传递给对 Task.Delay 的所有调用,并在关闭时设置取消令牌。请注意,取消也会导致您必须围绕 Task.Delay 处理的异常
        【解决方案9】:

        我知道我在这个聚会上迟到了几十万分钟,但我想我会加我的 2 美分。我很惊讶没有人提出这个建议,所以我假设有一些我不知道的东西可能会使它不太理想,所以如果它被击落,也许我会学到一些新东西。 我经常使用使用System.Threading.TimerChange()方法的解决方案。

        using System.Threading;
        
        Timer delayedActionTimer;
        
        public MyClass()
        {
            // Setup our timer
            delayedActionTimer = new Timer(saveOrWhatever, // The method to call when triggered
                                           null, // State object (Not required)
                                           Timeout.Infinite, // Start disabled
                                           Timeout.Infinite); // Don't repeat the trigger
        }
        
        // A change was made that we want to save but not until a
        // reasonable amount of time between changes has gone by
        // so that we're not saving on every keystroke/trigger event.
        public void TextChanged()
        {
            delayedActionTimer.Change(3000, // Trigger this timers function in 3 seconds,
                                            // overwriting any existing countdown
                                      Timeout.Infinite); // Don't repeat this trigger; Only fire once
        }
        
        // Timer requires the method take an Object which we've set to null since we don't
        // need it for this example
        private void saveOrWhatever(Object obj) 
        {
            /*Do the thing*/
        }
        

        【讨论】:

        • 计时器对于需要的东西来说有点矫枉过正,其他示例仅在需要时执行代码,而无论发生任何事件,它都会继续执行代码。
        • 这不会连续执行代码。当TextChanged 被调用时,它会安排saveOrWhatever 方法在3 秒后被调用。如果在调用saveOrWhatever 之前再次调用TextChanged,它会重置计时器,因此它只会在最后一次TextChanged 调用后3 秒后调用。定时器对象上Change的第二个参数是重触发率。当设置为无限时,它不会在第一次代码执行后重新触发。
        • 它不断地执行一个定时器。
        【解决方案10】:

        这个小宝石的灵感来自于 Mike Wards 的恶魔般巧妙的extension 尝试。但是,这个清理起来非常好。

        public static Action Debounce(this Action action, int milliseconds = 300)
        {
            CancellationTokenSource lastCToken = null;
        
            return () =>
            {
                //Cancel/dispose previous
                lastCToken?.Cancel();
                try { 
                    lastCToken?.Dispose(); 
                } catch {}          
        
                var tokenSrc = lastCToken = new CancellationTokenSource();
        
                Task.Delay(milliseconds).ContinueWith(task => { action(); }, tokenSrc.Token);
            };
        }
        

        注意:在这种情况下无需处理任务。证据见here

        用法

        Action DebounceToConsole;
        int count = 0;
        
        void Main()
        {
            //Assign
            DebounceToConsole = ((Action)ToConsole).Debounce(50);
        
            var random = new Random();
            for (int i = 0; i < 50; i++)
            {
                DebounceToConsole();
                Thread.Sleep(random.Next(100));
            }
        }
        
        public void ToConsole()
        {
            Console.WriteLine($"I ran for the {++count} time.");
        }
        

        【讨论】:

        • 好主意。它是否需要在 Cancel/Dispose/new/assign 部分周围设置围栏?此外,关于任务处置的良好参考。我总是担心僵尸任务。我想我现在可以少一点烦恼了。谢谢。
        • 嗯。检查 CancellationTokenSource 文档,显然它是线程安全的,除了 Dispose() “只能在 CancellationTokenSource 对象上的所有其他操作都完成时使用。” .我相信在这个用例中在 Cancel() 之后 Dispose() 是安全的。但是,他们确实建议将在此上下文中使用的 Dispose() 包装在 Try-Catch 中。我会补充的。
        【解决方案11】:

        这是受 Nieminen 基于 Task.Delay 的 Debouncer class 启发的。简化,一些小的更正,应该更好地自行清理。

        class Debouncer: IDisposable
        {
            private CancellationTokenSource lastCToken;
            private int milliseconds;
        
            public Debouncer(int milliseconds = 300)
            {
                this.milliseconds = milliseconds;
            }
        
            public void Debounce(Action action)
            {
                Cancel(lastCToken);
        
                var tokenSrc = lastCToken = new CancellationTokenSource();
        
                Task.Delay(milliseconds).ContinueWith(task =>
                {
                     action();
                }, 
                    tokenSrc.Token
                );
            }
        
            public void Cancel(CancellationTokenSource source)
            {
                if (source != null)
                {
                    source.Cancel();
                    source.Dispose();
                }                 
            }
        
            public void Dispose()
            {
                Cancel(lastCToken);
            }
        
            ~Debouncer()
            {
                Dispose();
            }
        }
        

        用法

        private Debouncer debouncer = new Debouncer(500); //1/2 a second
        ...
        debouncer.Debounce(SomeAction);
        

        【讨论】:

        • 这太棒了。我根据您的解决方案写了另一个答案,它使用唯一的字符串键进行节流(而不是实际的 Action),这对 webapps stackoverflow.com/a/64867741/56621很有用
        • 我写了一个 is async 没有续写 - 适合在 aspnet 中使用
        【解决方案12】:

        我需要 Blazor 的 Debounce 方法并不断返回此页面,因此我想分享我的解决方案以防它帮助其他人。

        public class DebounceHelper
        {
            private CancellationTokenSource debounceToken = null;
        
            public async Task DebounceAsync(Func<CancellationToken, Task> func, int milliseconds = 1000)
            {
                try
                {
                    // Cancel previous task
                    if (debounceToken != null) { debounceToken.Cancel(); }
        
                    // Assign new token
                    debounceToken = new CancellationTokenSource();
        
                    // Debounce delay
                    await Task.Delay(milliseconds, debounceToken.Token);
        
                    // Throw if canceled
                    debounceToken.Token.ThrowIfCancellationRequested();
        
                    // Run function
                    await func(debounceToken.Token);
                }
                catch (TaskCanceledException) { }
            }
        }
        

        搜索函数调用示例

        <input type="text" @oninput=@(async (eventArgs) => await OnSearchInput(eventArgs)) />
        
        @code {
            private readonly DebounceHelper debouncer = new DebounceHelper();
        
            private async Task OnSearchInput(ChangeEventArgs eventArgs)
            {
                await debouncer.DebounceAsync(async (cancellationToken) =>
                {
                    // Search Code Here         
                });
            }
        }
        
        

        【讨论】:

          【解决方案13】:

          想出了如何使用 System.Reactive NuGet 包对 TextBox 进行适当的去抖动。

          在班级层面,我们有自己的领域

          private IObservable<EventPattern<TextChangedEventArgs>> textChanged;
          

          那么当我们要开始监听事件时:

          // Debouncing capability
          textChanged = Observable.FromEventPattern<TextChangedEventArgs>(txtSearch, "TextChanged");
          textChanged.ObserveOnDispatcher().Throttle(TimeSpan.FromSeconds(1)).Subscribe(args => {
              Debug.WriteLine("bounce!");
          });
          

          确保您没有将文本框也连接到事件处理程序。上面的 Lambda 是事件处理程序。

          【讨论】:

            【解决方案14】:

            我需要这样的东西,但在 web 应用程序中,所以我不能将 Action 存储在变量中,它会在 http 请求之间丢失。

            基于其他答案和@Collie 的想法,我创建了一个类,该类查看唯一的字符串键以进行节流。

            public static class Debouncer
            {
                static ConcurrentDictionary<string, CancellationTokenSource> _tokens = new ConcurrentDictionary<string, CancellationTokenSource>();
                public static void Debounce(string uniqueKey, Action action, int seconds)
                {
                    var token = _tokens.AddOrUpdate(uniqueKey,
                        (key) => //key not found - create new
                        {
                            return new CancellationTokenSource();
                        },
                        (key, existingToken) => //key found - cancel task and recreate
                        {
                            existingToken.Cancel(); //cancel previous
                            return new CancellationTokenSource();
                        }
                    );
            
                    Task.Delay(seconds * 1000, token.Token).ContinueWith(task =>
                    {
                        if (!task.IsCanceled)
                        {
                            action();
                            _tokens.TryRemove(uniqueKey, out _);
                        }
                    }, token.Token);
                }
            }
            

            用法:

            //throttle for 5 secs if it's already been called with this KEY
            Debouncer.Debounce("Some-Unique-ID", () => SendEmails(), 5);
            

            作为一个额外的好处,因为它基于字符串键,你可以使用 inline lambda's

            Debouncer.Debounce("Some-Unique-ID", () => 
            {
                //do some work here
            }, 5);
            

            【讨论】:

              【解决方案15】:

              我编写了一个不运行异步同步的异步去抖动器。

              public sealed class Debouncer : IDisposable {
              
                public Debouncer(TimeSpan? delay) => _delay = delay ?? TimeSpan.FromSeconds(2);
              
                private readonly TimeSpan _delay;
                private CancellationTokenSource? previousCancellationToken = null;
              
                public async Task Debounce(Action action) {
                  _ = action ?? throw new ArgumentNullException(nameof(action));
                  Cancel();
                  previousCancellationToken = new CancellationTokenSource();
                  try {
                    await Task.Delay(_delay, previousCancellationToken.Token);
                    await Task.Run(action, previousCancellationToken.Token);
                  }
                  catch (TaskCanceledException) { }    // can swallow exception as nothing more to do if task cancelled
                }
              
                public void Cancel() {
                  if (previousCancellationToken != null) {
                    previousCancellationToken.Cancel();
                    previousCancellationToken.Dispose();
                  }
                }
              
                public void Dispose() => Cancel();
              
              }
              

              我用它来消除文件更改报告的更改,请参阅完整示例 here

              【讨论】:

                【解决方案16】:

                我受到 Mike 的回答的启发,但需要无需任务即可工作的解决方案,它只会吞噬后续事件调用,直到去抖动超时用完。这是我的解决方案:

                public static Action<T> Debounce<T>(this Action<T> action, int milliseconds = 300)
                {
                    DateTime? runningCallTime = null;
                    var locker = new object();
                
                    return arg =>
                    {
                        lock (locker)
                        {
                            if (!runningCallTime.HasValue ||
                                runningCallTime.Value.AddMilliseconds(milliseconds) <= DateTime.UtcNow)
                            {
                                runningCallTime = DateTime.UtcNow;
                                action.Invoke(arg);
                            }
                        }
                    };
                
                }
                

                【讨论】:

                  【解决方案17】:

                  为等待调用创建了这个类来解决它:

                  public class Debouncer
                  {
                      private CancellationTokenSource _cancelTokenSource = null;
                  
                      public async Task Debounce(Func<Task> method, int milliseconds = 300)
                      {
                          _cancelTokenSource?.Cancel();
                          _cancelTokenSource?.Dispose();
                  
                          _cancelTokenSource = new CancellationTokenSource();
                  
                          await Task.Delay(milliseconds, _cancelTokenSource.Token);
                  
                          await method();
                      }
                  }
                  

                  使用示例:

                  private Debouncer _debouncer = new Debouncer();
                  ....
                  await _debouncer.Debounce(YourAwaitableMethod);
                  

                  【讨论】:

                    【解决方案18】:

                    我根据@Mike Ward 的回答做了一些更简单的解决方案:

                    public static class CustomTaskExtension
                    {
                        #region fields
                    
                        private static int _last = 0;
                    
                        #endregion
                    
                        public static void Debounce(CancellationTokenSource throttleCts, double debounceTimeMs, Action action)
                        {
                            var current = Interlocked.Increment(ref _last);
                            Task.Delay(TimeSpan.FromMilliseconds(debounceTimeMs), throttleCts.Token)
                                .ContinueWith(task =>
                                {
                                    if (current == _last) action();
                                    task.Dispose();
                                });
                        }
                    }
                    

                    使用示例:

                    // security way to cancel the debounce process any time
                    CancellationTokenSource _throttleCts = new CancellationTokenSource();
                    
                    public void MethodCalledManyTimes()
                    {
                        // will wait 250ms after the last call
                        CustomTaskExtension.Debounce(_throttleCts, 250, async () =>
                        {
                            Console.Write("Execute your code 250ms after the last call.");
                        });
                    }
                    

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 2016-07-24
                      • 1970-01-01
                      • 1970-01-01
                      • 2019-12-21
                      • 1970-01-01
                      • 2021-02-05
                      • 1970-01-01
                      相关资源
                      最近更新 更多