Может кто-нибудь объяснить функцию "debounce" в Javascript

меня интересует функция "debouncing" в 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);
    };
};

EDIT: ранее скопированный фрагмент кода имел callNow в неположенном месте.

5 ответов


код в вопросе был немного изменен из кода в ссылке. В ссылке, есть чек на (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);  
     }; 
};

здесь важно отметить, что debounce производит функции это "закрыто над"timeout переменной. The timeout переменная остается доступной во время каждого вызова производимой функции даже после debounce сама вернулась, и можете изменение различных вызовов.

общая идея debounce следующий:

  1. пуск без ожидания.
  2. если производимая функция вызывается, очистите и сбросьте время ожидания.
  3. если время ожидания выбрано, вызовите исходную функцию.

первый пункт просто var timeout;, это действительно просто undefined. К счастью,clearTimeout довольно слабо о его входе: передача undefined идентификатор таймера заставляет его просто ничего не делать, он не выдает ошибку или что-то еще.

вторая точка выполняется производимой функцией. Сначала он хранит некоторую информацию о вызове (this контекст и arguments) в переменных, так что позже можете использовать их для debounced звонок. Затем он очищает тайм-аут (если был один набор), а затем создает новый, чтобы заменить его с помощью setTimeout. обратите внимание, что это заменяет значение timeout и это значение сохраняется в течение нескольких вызовов функций! это позволяет debounce фактически работать: если функция вызывается несколько раз,timeout перезаписывается несколько раз, с новым таймером. Если бы это было не так, несколько вызовов вызывали бы несколько таймеров для запуска, которые все остаются активными - звонки просто задержаться, но не debounced.

третья точка выполняется в обратном вызове тайм-аута. Это расстраивает timeout переменная и выполняет фактический вызов функции, используя сохраненную информацию о вызове.

на immediate флаг должен контролировать, должна ли функция вызываться до или после таймер. Если это false оригинал функция не вызывается до после таймер включен. Если это true исходная функция первый вызывается и больше не будет вызываться, пока таймер не будет нажат.

однако я считаю, что if (immediate && !timeout) проверить не так: timeout только что был установлен в идентификатор таймера, возвращаемый setTimeout так !timeout всегда false в этот момент и, следовательно, функция никогда не может быть вызвана. текущая версия подчеркивания.js кажется, есть немного другая проверка, где она оценивает immediate && !timeout до вызов setTimeout. (Алгоритм также немного отличается, например, он не использует clearTimeout.) Вот почему вы всегда должны пытаться использовать последнюю версию своих библиотек. :-)


Деблокированные функции не выполняются при вызове, они ждут паузы вызовов в течение настраиваемой продолжительности перед выполнением; каждый новый вызов перезапускает таймер.

дроссельные функции выполняются, а затем ждут настраиваемой продолжительности, прежде чем иметь право на повторный запуск.

Debounce отлично подходит для событий нажатия клавиш; когда пользователь начинает печатать, а затем делает паузу, вы отправляете все нажатия клавиш как одно событие, тем самым сокращая обработку вызовы.

дроссель отлично подходит для конечных точек в реальном времени, которые вы хотите разрешить пользователю вызывать только один раз за заданный период времени.

проверить подчеркивания.js для их реализации тоже.


Я написал пост под названием демистификация Debounce в JavaScript где я объясняю точно как работает функция debounce и включить демо.

Я тоже не совсем понимал, как работает функция 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);
        }
    };
};

то, что вы хотите сделать, это следующее: Если вы пытаетесь вызвать функцию сразу после другой, первая должна быть отменен и новый должен дождаться заданного таймаута,а затем выполнить. Таким образом, вам нужен какой-то способ отмены тайм-аута первой функции? Но как? Вы мог бы вызовите функцию и передайте возвращаемый timeout-id, а затем передайте этот ID в любые новые функции. Но решение выше является более элегантным.

что это делает это эффективно сделать timeout переменная, доступная в области возвращаемой функции. Поэтому, когда запускается событие "resize", оно не вызывает debounce() опять же, отсюда timeout содержимое не изменяется (!) и по-прежнему доступны для "следующего вызова функции".

ключевым моментом здесь является то, что мы вызываем внутреннюю функцию каждый раз, когда у нас есть событие изменения размера. Возможно, более ясно, если мы представим, что все события 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 to content и arguments to args.