【问题标题】:Can someone explain the "debounce" function in Javascript有人可以解释 Javascript 中的“去抖动”功能吗
【发布时间】:2021-04-30 02:53:51
【问题描述】:

我对javascript中的“去抖动”功能感兴趣,写在这里:http://davidwalsh.name/javascript-debounce-function

不幸的是,代码解释得不够清楚,我无法理解。谁能帮我弄清楚它是如何工作的(我在下面留下了我的 cmets)。总之我真的不明白这是怎么回事

   // Returns a function, that, as long as it continues to be invoked, will not
   // be triggered. The function will be called after it stops being called for
   // N milliseconds.


function debounce(func, wait, immediate) {
    var timeout;
    return function() {
        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
};

编辑:复制的代码 sn-p 以前在错误的位置有 callNow

【问题讨论】:

  • 如果您调用 clearTimeout 时使用的不是有效的计时器 ID,它不会执行任何操作。
  • @false,这是有效的标准行为吗?
  • @Pacerier 是的,它是in the spec:“如果句柄在调用该方法的WindowTimers 对象的活动计时器列表中没有标识条目,则该方法什么也不做。 "
  • 这个博客很有帮助loopinfinito

标签: javascript debouncing


【解决方案1】:

问题中的代码与链接中的代码略有不同。在链接中,在创建新的超时之前检查了(immediate && !timeout)。拥有它之后会导致立即模式永远不会触发。我已更新我的答案以从链接中注释工作版本。

function debounce(func, wait, immediate) {
  // 'private' variable for instance
  // The returned function will be able to reference this due to closure.
  // Each call to the returned function will share this common timer.
  var timeout;

  // Calling debounce returns a new anonymous function
  return function() {
    // reference the context and args for the setTimeout function
    var context = this,
      args = arguments;

    // Should the function be called now? If immediate is true
    //   and not already in a timeout then the answer is: Yes
    var callNow = immediate && !timeout;

    // This is the basic debounce behaviour where you can call this 
    //   function several times, but it will only execute once 
    //   [before or after imposing a delay]. 
    //   Each time the returned function is called, the timer starts over.
    clearTimeout(timeout);

    // Set the new timeout
    timeout = setTimeout(function() {

      // Inside the timeout function, clear the timeout variable
      // which will let the next execution run when in 'immediate' mode
      timeout = null;

      // Check if the function already ran with the immediate flag
      if (!immediate) {
        // Call the original function with apply
        // apply lets you define the 'this' object as well as the arguments 
        //    (both captured before setTimeout)
        func.apply(context, args);
      }
    }, wait);

    // Immediate mode and no wait timer? Execute the function..
    if (callNow) func.apply(context, args);
  }
}

/////////////////////////////////
// DEMO:

function onMouseMove(e){
  console.clear();
  console.log(e.x, e.y);
}

// Define the debounced function
var debouncedMouseMove = debounce(onMouseMove, 50);

// Call the debounced function on every mouse move
window.addEventListener('mousemove', debouncedMouseMove);

【讨论】:

  • 用于immediate && timeout 检查。不会总是有timeout (因为timeout 更早被调用)。另外,clearTimeout(timeout) 有什么好处,当它被声明(使其未定义)并被清除时,更早
  • immediate && !timeout 检查用于当使用immediate 标志配置去抖动时。这将立即执行该函数,但在 if 可以再次执行之前施加 wait 超时。所以!timeout 部分基本上是在说“对不起,小伙子,这已经在定义的窗口内执行了”...记住 setTimeout 函数将清除它,允许执行下一次调用。
  • 为什么必须在setTimeout 函数中将超时设置为空?另外,我已经尝试过这段代码,对我来说,传入true for immediate 只会阻止函数被调用(而不是在延迟后被调用)。你会遇到这种情况吗?
  • 我有一个关于立即的类似问题?为什么它需要立即参数。将 wait 设置为 0 应该具有相同的效果,对吧?正如@Startec 提到的,这种行为很奇怪。
  • 如果您只是调用该函数,那么您不能在再次调用该函数之前强加一个等待计时器。想象一个用户捣碎开火键的游戏。您希望立即触发该火灾,但无论用户按下按钮的速度有多快,都不会在 X 毫秒内再次触发。
【解决方案2】:

这里要注意的重要一点是debounce 产生一个函数,它“封闭”了timeout 变量。 timeout 变量在每次调用生成的函数期间保持可访问性,即使在 debounce 本身返回之后,并且可以切换不同的调用。

debounce 的总体思路如下:

  1. 开始时没有超时。
  2. 如果调用了生成的函数,清除并重置超时。
  3. 如果超时,调用原函数。

第一点就是var timeout;,确实就是undefined。幸运的是,clearTimeout 对它的输入相当宽松:传递一个 undefined 计时器标识符会导致它什么也不做,它不会抛出错误或其他什么。

第二点是由produced函数完成的。它首先将有关调用的一些信息(this 上下文和arguments)存储在变量中,以便以后可以将这些信息用于去抖动调用。然后它会清除超时(如果有一组),然后使用setTimeout 创建一个新的来替换它。 请注意,这会覆盖 timeout 的值,并且该值会在多个函数调用中持续存在! 这允许去抖动实际工作:如果函数被多次调用,timeout 将被多次覆盖一个新的计时器。如果不是这种情况,多个调用将导致启动多个计时器,而 所有 保持活动状态 - 调用只会被延迟,但不会被消除抖动。

第三点在超时回调中完成。它取消设置timeout 变量并使用存储的调用信息进行实际的函数调用。

immediate 标志应该控制是否应该在计时器之前之后调用该函数。如果是false,则在计时器被击中之后之前不会调用原始函数。如果是true,则首先调用原来的函数,直到定时器被击中才会被调用。

但是,我确实认为 if (immediate && !timeout) 检查是错误的:timeout 刚刚设置为 setTimeout 返回的计时器标识符,因此 !timeout 在那时始终是 false,因此该函数可以永远不会被调用。 The current version of underscore.js 似乎有一个稍微不同的检查,它在调用setTimeout 之前评估immediate && !timeout 之前。 (算法也有点不同,例如它不使用clearTimeout。)这就是为什么您应该始终尝试使用最新版本的库。 :-)

【讨论】:

  • "请注意,这会覆盖 timeout 的值,并且该值会在多个函数调用中持续存在" 不是每个 debounce 调用的本地超时吗?它是用 var 声明的。每次是如何覆盖的?另外,为什么最后要检查!timeout?为什么不一直存在(因为设置为setTimeout(function() etc.)
  • @Startec 对于debounce 的每次调用都是本地的,是的,但它在对返回函数的调用之间共享(这是你要调用的函数采用)。例如,在g = debounce(f, 100) 中,timeout 的值会在多次调用g 时保持不变。最后的!timeout检查我相信是一个错误,它不在当前的underscore.js代码中。
  • 为什么超时需要在return函数的早期清除(在它被声明之后)?此外,它会在 setTimeout 函数内部设置为 null。这不是多余的吗? (首先将其清除,然后将其设置为null。在我使用上述代码的测试中,将 immediate 设置为 true 会使函数根本不调用,正如您所提到的。没有下划线的任何解决方案?
【解决方案3】:

去抖函数在调用时不会执行,它们在执行前等待调用暂停超过可配置的持续时间;每次新的调用都会重新启动计时器。

受限制的函数会执行,然后等待一段可配置的持续时间,然后才能再次触发。

去抖动非常适合按键事件;当用户开始输入然后暂停时,您将所有按键作为单个事件提交,从而减少了处理调用。

Throttle 非常适合您只希望允许用户在设定的时间段内调用一次的实时端点。

查看Underscore.js 了解他们的实现。

【讨论】:

    【解决方案4】:

    我写了一篇题为 Demistifying Debounce in JavaScript 的帖子,我在其中准确地解释了 how a debounce function works 并包含了一个演示。

    当我第一次遇到去抖功能时,我也没有完全理解它是如何工作的。虽然体积相对较小,但它们实际上采用了一些非常先进的 JavaScript 概念!掌握范围、闭包和setTimeout 方法会有所帮助。

    话虽如此,下面是我在上面引用的帖子中解释和演示的基本去抖动功能。

    成品

    // Create JD Object
    // ----------------
    var JD = {};
    
    // Debounce Method
    // ---------------
    JD.debounce = function(func, wait, immediate) {
        var timeout;
        return function() {
            var context = this,
                args = arguments;
            var later = function() {
                timeout = null;
                if ( !immediate ) {
                    func.apply(context, args);
                }
            };
            var callNow = immediate && !timeout;
            clearTimeout(timeout);
            timeout = setTimeout(later, wait || 200);
            if ( callNow ) { 
                func.apply(context, args);
            }
        };
    };
    

    解释

    // Create JD Object
    // ----------------
    /*
        It's a good idea to attach helper methods like `debounce` to your own 
        custom object. That way, you don't pollute the global space by 
        attaching methods to the `window` object and potentially run in to
        conflicts.
    */
    var JD = {};
    
    // Debounce Method
    // ---------------
    /*
        Return a function, that, as long as it continues to be invoked, will
        not be triggered. The function will be called after it stops being 
        called for `wait` milliseconds. If `immediate` is passed, trigger the 
        function on the leading edge, instead of the trailing.
    */
    JD.debounce = function(func, wait, immediate) {
        /*
            Declare a variable named `timeout` variable that we will later use 
            to store the *timeout ID returned by the `setTimeout` function.
    
            *When setTimeout is called, it retuns a numeric ID. This unique ID
            can be used in conjunction with JavaScript's `clearTimeout` method 
            to prevent the code passed in the first argument of the `setTimout`
            function from being called. Note, this prevention will only occur
            if `clearTimeout` is called before the specified number of 
            milliseconds passed in the second argument of setTimeout have been
            met.
        */
        var timeout;
    
        /*
            Return an anomymous function that has access to the `func`
            argument of our `debounce` method through the process of closure.
        */
        return function() {
    
            /*
                1) Assign `this` to a variable named `context` so that the 
                   `func` argument passed to our `debounce` method can be 
                   called in the proper context.
    
                2) Assign all *arugments passed in the `func` argument of our
                   `debounce` method to a variable named `args`.
    
                *JavaScript natively makes all arguments passed to a function
                accessible inside of the function in an array-like variable 
                named `arguments`. Assinging `arguments` to `args` combines 
                all arguments passed in the `func` argument of our `debounce` 
                method in a single variable.
            */
            var context = this,   /* 1 */
                args = arguments; /* 2 */
    
            /*
                Assign an anonymous function to a variable named `later`.
                This function will be passed in the first argument of the
                `setTimeout` function below.
            */
            var later = function() {
    
                /*      
                    When the `later` function is called, remove the numeric ID 
                    that was assigned to it by the `setTimeout` function.
    
                    Note, by the time the `later` function is called, the
                    `setTimeout` function will have returned a numeric ID to 
                    the `timeout` variable. That numeric ID is removed by 
                    assiging `null` to `timeout`.
                */
                timeout = null;
    
                /*
                    If the boolean value passed in the `immediate` argument 
                    of our `debouce` method is falsy, then invoke the 
                    function passed in the `func` argument of our `debouce`
                    method using JavaScript's *`apply` method.
    
                    *The `apply` method allows you to call a function in an
                    explicit context. The first argument defines what `this`
                    should be. The second argument is passed as an array 
                    containing all the arguments that should be passed to 
                    `func` when it is called. Previously, we assigned `this` 
                    to the `context` variable, and we assigned all arguments 
                    passed in `func` to the `args` variable.
                */
                if ( !immediate ) {
                    func.apply(context, args);
                }
            };
    
            /*
                If the value passed in the `immediate` argument of our 
                `debounce` method is truthy and the value assigned to `timeout`
                is falsy, then assign `true` to the `callNow` variable.
                Otherwise, assign `false` to the `callNow` variable.
            */
            var callNow = immediate && !timeout;
    
            /*
                As long as the event that our `debounce` method is bound to is 
                still firing within the `wait` period, remove the numerical ID  
                (returned to the `timeout` vaiable by `setTimeout`) from 
                JavaScript's execution queue. This prevents the function passed 
                in the `setTimeout` function from being invoked.
    
                Remember, the `debounce` method is intended for use on events
                that rapidly fire, ie: a window resize or scroll. The *first* 
                time the event fires, the `timeout` variable has been declared, 
                but no value has been assigned to it - it is `undefined`. 
                Therefore, nothing is removed from JavaScript's execution queue 
                because nothing has been placed in the queue - there is nothing 
                to clear.
    
                Below, the `timeout` variable is assigned the numerical ID 
                returned by the `setTimeout` function. So long as *subsequent* 
                events are fired before the `wait` is met, `timeout` will be 
                cleared, resulting in the function passed in the `setTimeout` 
                function being removed from the execution queue. As soon as the 
                `wait` is met, the function passed in the `setTimeout` function 
                will execute.
            */
            clearTimeout(timeout);
    
            /*
                Assign a `setTimout` function to the `timeout` variable we 
                previously declared. Pass the function assigned to the `later` 
                variable to the `setTimeout` function, along with the numerical 
                value assigned to the `wait` argument in our `debounce` method. 
                If no value is passed to the `wait` argument in our `debounce` 
                method, pass a value of 200 milliseconds to the `setTimeout` 
                function.  
            */
            timeout = setTimeout(later, wait || 200);
    
            /*
                Typically, you want the function passed in the `func` argument
                of our `debounce` method to execute once *after* the `wait` 
                period has been met for the event that our `debounce` method is 
                bound to (the trailing side). However, if you want the function 
                to execute once *before* the event has finished (on the leading 
                side), you can pass `true` in the `immediate` argument of our 
                `debounce` method.
    
                If `true` is passed in the `immediate` argument of our 
                `debounce` method, the value assigned to the `callNow` variable 
                declared above will be `true` only after the *first* time the 
                event that our `debounce` method is bound to has fired.
    
                After the first time the event is fired, the `timeout` variable
                will contain a falsey value. Therfore, the result of the 
                expression that gets assigned to the `callNow` variable is 
                `true` and the function passed in the `func` argument of our
                `debounce` method is exected in the line of code below.
    
                Every subsequent time the event that our `debounce` method is 
                bound to fires within the `wait` period, the `timeout` variable 
                holds the numerical ID returned from the `setTimout` function 
                assigned to it when the previous event was fired, and the 
                `debounce` method was executed.
    
                This means that for all subsequent events within the `wait`
                period, the `timeout` variable holds a truthy value, and the
                result of the expression that gets assigned to the `callNow`
                variable is `false`. Therefore, the function passed in the 
                `func` argument of our `debounce` method will not be executed.  
    
                Lastly, when the `wait` period is met and the `later` function
                that is passed in the `setTimeout` function executes, the 
                result is that it just assigns `null` to the `timeout` 
                variable. The `func` argument passed in our `debounce` method 
                will not be executed because the `if` condition inside the 
                `later` function fails. 
            */
            if ( callNow ) { 
                func.apply(context, args);
            }
        };
    };
    

    【讨论】:

    • 最佳解释。谢谢
    【解决方案5】:

    我们现在都在使用 Promises

    我见过的许多实现都使问题过于复杂或存在其他卫生问题。现在是 2021 年,我们已经使用 Promises 很长时间了——而且有充分的理由。 Promise 清理异步程序并减少发生错误的机会。在这篇文章中,我们将编写自己的debounce。这个实现将 -

    • 在任何给定时间最多有一个待处理的承诺(每个去抖动任务)
    • 通过正确取消待处理的承诺来阻止内存泄漏
    • 仅解决最新的承诺
    • 通过实时代码演示展示正确的行为

    我们用它的两个参数编写debouncetask 去抖动,以及延迟的毫秒数,ms。我们为其本地状态引入单个本地绑定,t -

    function debounce (task, ms) {
      let t = { promise: null, cancel: _ => void 0 }
      return async (...args) => {
        try {
          t.cancel()
          t = deferred(ms)
          await t.promise
          await task(...args)
        }
        catch (_) { /* prevent memory leak */ }
      }
    }
    

    我们依赖于一个可重用的deferred 函数,它创建了一个在ms 毫秒内解析的新承诺。它引入了两个本地绑定,promise 本身,cancel 它的能力 -

    function deferred (ms) {
      let cancel, promise = new Promise((resolve, reject) => {
        cancel = reject
        setTimeout(resolve, ms)
      })
      return { promise, cancel }
    }
    

    点击计数器示例

    在第一个示例中,我们有一个计算用户点击次数的按钮。事件侦听器使用debounce 附加,因此计数器仅在指定持续时间后递增 -

    // debounce, deferred
    function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } }
    function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }
    
    // dom references
    const myform = document.forms.myform
    const mycounter = myform.mycounter
    
    // event handler
    function clickCounter (event) {
      mycounter.value = Number(mycounter.value) + 1
    }
    
    // debounced listener
    myform.myclicker.addEventListener("click", debounce(clickCounter, 1000))
    <form id="myform">
    <input name="myclicker" type="button" value="click" />
    <output name="mycounter">0</output>
    </form>

    实时查询示例,“自动完成”

    在第二个示例中,我们有一个带有文本输入的表单。我们的search 查询是使用debounce 附加的-

    // debounce, deferred
    function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } }
    function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }
    
    // dom references
    const myform = document.forms.myform
    const myresult = myform.myresult
    
    // event handler
    function search (event) {
      myresult.value = `Searching for: ${event.target.value}`
    }
    
    // debounced listener
    myform.myquery.addEventListener("keypress", debounce(search, 1000))
    <form id="myform">
    <input name="myquery" placeholder="Enter a query..." />
    <output name="myresult"></output>
    </form>

    【讨论】:

    • 2021年我们不是都在使用Typescript吗?
    • 不,打字稿是业余爱好者设计类型系统时得到的。编程和技术充满了因各种错误原因而流行的坏事的例子。不要误以为受欢迎是好事。
    【解决方案6】:

    你想要做的是:如果你试图一个接一个地调用一个函数,第一个应该被取消,新的应该等待一个给定的超时然后执行。所以实际上你需要某种方式来取消第一个函数的超时?但是怎么做? 您可以调用该函数,并传递返回的 timeout-id,然后将该 ID 传递给任何新函数。但是上面的解决方案更加优雅。

    它的作用是有效地使timeout 变量在返回函数的范围内可用。因此,当触发 'resize' 事件时,它不会再次调用 debounce(),因此 timeout 内容不会更改(!)并且仍然可用于“下一个函数调用”。

    这里的关键基本上是每次我们有一个resize事件时我们都会调用内部函数。如果我们想象所有的调整大小事件都在一个数组中,也许会更清楚:

    var events = ['resize', 'resize', 'resize'];
    var timeout = null;
    for (var i = 0; i < events.length; i++){
        if (immediate && !timeout) func.apply(this, arguments);
        clearTimeout(timeout); // does not do anything if timeout is null.
        timeout = setTimeout(function(){
            timeout = null;
            if (!immediate) func.apply(this, arguments);
        }
    }
    

    您看到timeout 可用于下一次迭代吗? 在我看来,没有理由将 this 重命名为 contentarguments 重命名为 args

    【讨论】:

    • “重命名”是绝对必要的。 thisarguments 的含义在 setTimeout() 回调函数内部发生了变化。您必须在别处保留一份副本,否则该信息将丢失。
    【解决方案7】:

    这是一个变体,它总是在第一次调用时触发去抖动函数,并使用更具描述性的变量:

    function debounce(fn, wait = 1000) {
      let debounced = false;
      let resetDebouncedTimeout = null;
      return function(...args) {
        if (!debounced) {
          debounced = true;
          fn(...args);
          resetDebouncedTimeout = setTimeout(() => {
            debounced = false;
          }, wait);
        } else {
          clearTimeout(resetDebouncedTimeout);
          resetDebouncedTimeout = setTimeout(() => {
            debounced = false;
            fn(...args);
          }, wait);
        }
      }
    };
    

    【讨论】:

      【解决方案8】:

      javascript中的简单去抖动方法

      <!-- Basic HTML -->
      <!DOCTYPE html>
      <html>
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width">
        <title>Debounce Method</title>
      </head>
      <body>
        <button type="button" id="debounce">Debounce Method</button><br />
        <span id="message"></span>
      </body>
      </html>
      
        // JS File
        var debouncebtn = document.getElementById('debounce');
          function debounce(func, delay){
            var debounceTimer;
            return function () {
              var context = this, args = arguments;
              clearTimeout(debounceTimer);
              debounceTimer = setTimeout(function() {
                func.apply(context, args)
              }, delay);
            }
          }
      
      // Driver Code
      debouncebtn.addEventListener('click', debounce(function() {
          document.getElementById('message').innerHTML += '<br/> Button only triggeres is every 3 secounds how much every you fire an event';
        console.log('Button only triggeres in every 3 secounds how much every you fire an event');
      },3000))
      

      运行时示例 JSFiddle:https://jsfiddle.net/arbaazshaikh919/d7543wqe/10/

      【讨论】:

      • 不幸的是,我无法从部分 args = arguments 中理解。论据从何而来?我认为您需要将其设置为返回函数的参数
      【解决方案9】:

      简单的去抖动功能:-

      HTML:-

      <button id='myid'>Click me</button>
      

      Javascript:-

          function debounce(fn, delay) {
            let timeoutID;
            return function(...args){
                if(timeoutID) clearTimeout(timeoutID);
                timeoutID = setTimeout(()=>{
                  fn(...args)
                }, delay);
            }
         }
      
      document.getElementById('myid').addEventListener('click', debounce(() => {
        console.log('clicked');
      },2000));
      

      【讨论】:

        【解决方案10】:

        下面是debounce 函数作用的摘要,用几行演示解释。

        debounce 函数是这样的函数:

        • 在第一次执行时,使用setTimeout 函数安排包装函数在一段时间后执行
        • (如果在此时间间隔内再次执行):
          • 删除之前的计划(使用clearTimeOut函数)
          • 重新安排一个新的(使用setTimeout 函数)

        并且循环一直持续到时间间隔过去并且包装的函数执行。

        改编自所有 cmets 和 this article

        function debounce(callBack, interval, leadingExecution) {
        
        // the schedule identifier, if it's not null/undefined, a callBack function was scheduled
        let timerId;
        
        return function () {
        
            // Does the previous run has schedule a run
            let wasFunctionScheduled = (typeof timerId === 'number');
        
            // Delete the previous run (if timerId is null, it does nothing)
            clearTimeout(timerId);
        
            // Capture the environment (this and argument) and wraps the callback function
            let funcToDebounceThis = this, funcToDebounceArgs = arguments;
            let funcToSchedule = function () {
        
                // Reset/delete the schedule
                clearTimeout(timerId);
                timerId = null;
        
                // trailing execution happens at the end of the interval
                if (!leadingExecution) {
                    // Call the original function with apply
                    callBack.apply(funcToDebounceThis, funcToDebounceArgs);
                }
        
            }
        
            // Schedule a new execution at each execution
            timerId = setTimeout(funcToSchedule, interval);
        
            // Leading execution
            if (!wasFunctionScheduled && leadingExecution) callBack.apply(funcToDebounceThis, funcToDebounceArgs);
        
        }
        
        }
        
        function onMouseMove(e) {
        console.log(new Date().toLocaleString() + ": Position: x: " + e.x + ", y:" + e.y);
        }
        
        let debouncedMouseMove = debounce(onMouseMove, 500);
        
        document.addEventListener('mousemove', debouncedMouseMove);

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-01-08
          • 1970-01-01
          • 1970-01-01
          • 2011-06-24
          • 2021-05-27
          • 2020-04-20
          相关资源
          最近更新 更多