有人可以解释Java语言中的“反跳”功能吗


150

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

不幸的是,代码的解释不够清楚,我无法理解。谁能帮我弄清楚它是如何工作的(我在下面留了我的评论)。简而言之,我真的不明白这是如何工作的

   // 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);
    };
};

编辑:复制的代码片段以前callNow在错误的位置。


1
如果您clearTimeout使用不是有效计时器ID的名称进行调用,则它不会执行任何操作。
Ry-

@false,这是有效的标准行为吗?
Pacerier 2014年

3
@Pacerier是的,它在规范中:“如果句柄未在WindowTimers调用该方法的对象的活动计时器列表中标识一个条目,则该方法不执行任何操作。”
Mattias Buelens 2014年

Answers:


134

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

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);


1
针对immediate && timeout检查。不会总是有一个timeout(因为timeout早先被称为)。同样,clearTimeout(timeout)早在声明(使其未定义)和清除时,好的行为是做什么的
Startec

immediate && !timeout检查适用于何时使用immediate标志配置防抖动。这将立即执行该功能,但是wait如果可以再次执行,则会超时。因此,该!timeout部分基本上是说“抱歉,它已经在定义的窗口中执行了……”,请记住setTimeout函数将其清除,从而允许执行下一个调用。
2014年

1
为什么必须在setTimeout函数内部将超时设置为null ?另外,我已经尝试过这段代码,对我来说,true立即传入只是阻止了该函数的调用(而不是延迟后被调用)。这会发生在你身上吗?
Startec 2014年

我有一个关于即刻的类似问题吗?为什么它需要立即具有参数。将wait设置为0应该具有相同的效果,对吗?正如@Startec所提到的,这种行为很奇怪。
zeroliu15年

2
如果仅调用该函数,则无法强加一个等待计时器,然后才能再次调用该函数。想想一个游戏,用户会把火钥匙砸碎。您希望该起火立即触发,但是无论用户按动按钮的速度有多快,都不要再持续X毫秒再起火。
2015年

57

这里要注意的重要事情是debounce产生一个“封闭” 变量的函数timeouttimeout即使在变量debounce返回后,该变量在每次调用产生的函数期间仍可访问,并且可以在不同的调用之间进行切换。

的总体思路debounce如下:

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

第一点是正义var timeout;,的确是正义undefined。幸运的是,clearTimeout它的输入相当宽松:传递undefined计时器标识符会使它什么也不做,它不会引发错误或其他任何事情。

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

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

immediate标志应该控制是在计时器之前还是之后调用该函数。如果为false,则在点击计时器后才调用原始函数。如果为true,则将首先调用原始函数,并且在达到计时器之前不会再调用该函数。

但是,我确实认为if (immediate && !timeout)检查是错误的:timeout刚刚将其设置为由此返回的计时器标识符,setTimeout因此!timeout始终false位于该位置,因此永远无法调用该函数。当前版本的underscore.js似乎有一些不同的检查,它immediate && !timeout 调用之前对其进行评估setTimeout。(算法也有所不同,例如,它不使用clearTimeout。)因此,您应该始终尝试使用最新版本的库。:-)


“请注意,这将覆盖timeout的值,并且该值会在多个函数调用中持续存在”对于每个debounce调用来说,超时不是本地的吗?用var声明。每次如何覆盖?另外,为什么要最后检查!timeout?为什么它不总是存在(因为它设置为setTimeout(function() etc.)
Startec,2014年

2
@Startec是对每次调用本地的debounce,是的,但是在返回函数(这是您将要使用的函数)的调用之间共享。例如,在中g = debounce(f, 100),值会timeout在多次调用时持续存在g!timeout我相信最后的检查是一个错误,并且不在当前的underscore.js代码中。
Mattias Buelens 2014年

为什么需要在return函数的早期(声明后立即)清除超时?此外,然后在setTimeout函数中将其设置为null。这不是多余的吗?(首先将其清除,然后将其设置为null。在我使用上述代码进行的测试中,将
Instant

34

防反跳函数在调用时不会执行,它们会在可配置的持续时间内等待调用暂停。每次新的调用都会重新启动计时器。

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

防抖功能可用于按键事件。当用户开始键入然后暂停时,您将所有按键提交作为单个事件提交,从而减少了处理调用。

节流非常适合实时终结点,您只希望允许用户在设定的时间段内调用一次即可。

还要查看Underscore.js的实现。


24

在JavaScript中撰写了一篇题为Demistifying Debounce的文章,其中我确切地解释了去抖动功能的工作原理并包括一个演示。

当我第一次遇到反跳功能时,我也不太了解。尽管尺寸相对较小,但它们实际上采用了一些相当高级的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);
        }
    };
};

1

您要执行的操作如下:如果尝试立即调用另一个函数,则应取消第一个函数,而新的函数应等待给定的超时然后执行。因此,实际上,您需要某种方式来取消第一个函数的超时?但是如何?您可以调用该函数,并传递返回的超时ID,然后将该ID传递给任何新函数。但是上面的解决方案更优雅。

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

基本上,这里的关键是每次发生resize事件时,我们都会调用内部函数。如果我们想象所有的resize-events都在一个数组中,则可能更清楚:

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,以contentargumentsargs


“重命名”是绝对必要的。setTimeout()回调函数中thisand 的含义arguments更改。您必须在其他地方保留副本,否则该信息将丢失。
CubicleSoft

1

这是一个变体,它总是在第一次调用去抖动功能时就触发它,并带有更具描述性的变量:

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);
    }
  }
};

1

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/


0

简单的防抖功能:-

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));
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.