有人可以解释一下 Javascript 中的“去抖动”功能吗

IT技术 javascript debouncing
2021-01-18 10:03:47

我对 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位于错误的位置。

6个回答

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

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检查用于何时使用immediate标志配置去抖动这将立即执行该函数,但wait在可以再次执行之前强制执行超时。所以这!timeout部分基本上是在说“对不起,小伙子,这已经在定义的窗口内执行了”...记住 setTimeout 函数将清除它,允许下一次调用执行。
2021-03-12 10:03:47
我有一个关于立即的类似问题?为什么它需要立即参数。将等待设置为 0 应该具有相同的效果,对吗?正如@Startec 所提到的,这种行为非常奇怪。
2021-03-12 10:03:47
immediate && timeout支票。不会总是有一个timeout(因为timeout更早调用)。此外,什么时候好clearTimeout(timeout),当它被声明(使其未定义)和清除时,更早
2021-03-14 10:03:47
为什么超时必须在setTimeout函数内部设置为空另外,我已经尝试过这段代码,对我来说,传入truefor 立即只是防止函数被调用(而不是在延迟后被调用)。你会发生这种情况吗?
2021-03-27 10:03:47
如果您只是调用该函数,则在再次调用该函数之前,您不能强制使用等待计时器。想一想用户捣碎开火键的游戏。您希望该火立即触发,但无论用户按下按钮的速度有多快,都不会再触发 X 毫秒。
2021-03-28 10:03:47

这里要注意的重要一点是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刚刚设置为setTimeoutso返回的计时器标识符!timeout始终false在该点,因此永远无法调用该函数。underscore.js 的当前版本似乎有一个稍微不同的检查,它immediate && !timeout 调用setTimeout. (算法也有点不同,例如它不使用clearTimeout。)这就是为什么您应该始终尝试使用最新版本的库。:-)

“请注意,这会覆盖 timeout 的值,并且该值在多个函数调用中持续存在” 每个去抖动调用不是本地超时吗?它是用 var 声明的。怎么每次都覆盖?另外,为什么要!timeout在最后检查为什么它不总是存在(因为它被设置为setTimeout(function() etc.)
2021-03-17 10:03:47
为什么超时需要在返回函数中尽早清除(在声明之后)?此外,然后在 setTimeout 函数内将其设置为 null。这不是多余的吗?(首先它被清除,然后它被设置为null。在我对上面代码的测试中,设置为 true 使函数根本不调用,正如你所提到的。没有下划线的任何解决方案?
2021-03-26 10:03:47
@Startec 它对 的每次调用都是本地的debounce,是的,但它在对返回函数(这是您将要使用的函数)的调用之间共享例如,在 中g = debounce(f, 100), 的值在timeout对 的多次调用持续存在g最后的!timeout检查我相信是一个错误,它不在当前的 underscore.js 代码中。
2021-04-09 10:03:47

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

节流函数执行,然后等待一个可配置的持续时间,然后才有资格再次触发。

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

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

也可以查看Underscore.js的实现。

我写了一篇名为Demistifying Debounce in JavaScript 的文章,我其中准确解释了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);
        }
    };
};

我们现在都在使用 Promises

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

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

我们写debounce了它的两个参数,task去抖动和延迟的毫秒数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它引入了两个本地绑定,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 吗?
2021-03-11 10:03:47
不,typescript是业余爱好者设计类型系统时得到的。编程和技术充斥着由于各种错误原因而流行的坏事的例子。不要误认为流行是好的。
2021-03-27 10:03:47