누군가 Javascript에서 "디 바운스"기능을 설명 할 수 있습니까?


151

Javascript의 "debouncing"함수에 관심이 있습니다. 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 2016 년

3
@Pacerier 예, 사양에 있습니다 . "손잡이가 WindowTimers메소드가 호출 된 오브젝트 의 활성 타이머 목록에서 항목을 식별하지 않으면 메소드는 아무 것도 수행하지 않습니다."
Mattias Buelens 2016 년

답변:


134

문제의 코드는 링크의 코드와 약간 변경되었습니다. 링크에서 (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);


1
에 대한 immediate && timeout검사. 항상 호출 되지 timeout않기 때문 timeout입니다 (이전에 호출 되었기 때문 ). 또한 clearTimeout(timeout)선포 (정의되지 않음) 및
선고시

immediate && !timeout검사는 디 바운스가 구성 될 때입니다 immediate플래그. 이 기능은 즉시 실행되지만 wait다시 실행할 수 있으면 시간 초과가 발생합니다. 따라서 !timeout부분은 기본적으로 '죄송합니다. 이것은 이미 정의 된 창 내에서 실행되었습니다'라고 말하고 있습니다. setTimeout 함수가이를 지우면 다음 호출을 실행할 수 있습니다.
Malk

1
setTimeout함수 내에서 시간 초과를 null로 설정해야하는 이유는 무엇 입니까? 또한,이 코드를 시도해 보았습니다. true즉시 전달 하면 지연 후에 호출되는 것이 아니라 함수가 전혀 호출되지 않습니다. 이것이 당신을 위해 발생합니까?
Startec

나는 즉시에 대해 비슷한 질문이 있습니까? 즉각적인 매개 변수가 필요한 이유는 무엇입니까? wait를 0으로 설정하면 같은 효과가 있어야합니다. 그리고 @Startec이 언급 했듯이이 동작은 매우 이상합니다.
zeroliu

2
함수를 호출하면 해당 함수를 다시 호출하기 전에 대기 타이머를 강요 할 수 없습니다. 사용자가 발사 키를 으깨는 게임을 생각해보십시오. 해당 발사는 즉시 트리거되기를 원하지만 사용자가 버튼을 얼마나 빨리 으깨도 다른 X 밀리 초 동안 다시 발사하지 않습니다.
Malk

57

여기서 주목해야 할 것은 변수에 "닫힌" 함수debounce생성 한다는 것입니다 . 후에도 생산 함수의 모든 통화 중에 접근 할 변수 체류 자체가 돌아왔다, 그리고 서로 다른 통화를 통해 변경할 수 있습니다.timeouttimeoutdebounce

일반적인 아이디어 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.) 따라서 항상 최신 버전의 라이브러리를 사용해야합니다. :-)


"이것은 타임 아웃의 값을 덮어 쓰고이 값은 여러 함수 호출을 통해 지속된다는 점에 유의하십시오"타임 아웃이 각 디 바운스 호출에 국한되지 않습니까? var로 선언됩니다. 매번 어떻게 덮어 쓰나요? 또한 왜 !timeout마지막에 확인 합니까? 왜 존재하지 않는가 (왜냐하면setTimeout(function() etc.)
Startec

2
@Startec의 각 호출에 대해 로컬 debounce이지만, 반환 된 함수 (사용할 함수) 에 대한 호출간에 공유 됩니다. 예를 들어에서 g = debounce(f, 100)의 값은에 timeout대한 여러 번의 호출에서 지속됩니다 g. 마지막에 !timeout확인하는 것은 내가 생각하는 실수이며 현재 underscore.js 코드에는 없습니다.
Mattias Buelens 2016 년

리턴 함수에서 타임 아웃을 일찍 해제해야하는 이유는 무엇입니까 (선언 된 직후)? 또한 setTimeout 함수 내에서 null로 설정됩니다. 중복되지 않습니까? (먼저 지워지면 다음으로 설정됩니다 null. 위 코드를 사용한 테스트에서 즉시 true로 설정하면 함수가 전혀 호출되지 않습니다. 밑줄이없는 솔루션은 무엇입니까?
Startec

34

디 바운스 된 함수는 호출 될 때 실행되지 않으며 실행 전에 구성 가능한 기간 동안 호출 일시 중지를 기다립니다. 각각의 새로운 호출은 타이머를 다시 시작합니다.

조절 된 기능이 실행 된 다음 구성 가능한 기간 동안 기다렸다가 다시 발사 할 수 있습니다.

디 바운스는 키 누르기 이벤트에 적합합니다. 사용자가 입력을 시작한 다음 일시 중지하면 모든 키 누름을 단일 이벤트로 제출하여 처리 호출을 줄입니다.

스로틀은 사용자가 설정된 시간마다 한 번만 호출하도록 허용하려는 실시간 엔드 포인트에 적합합니다.

Underscore.js 의 구현도 확인하십시오 .


24

디 바운스 기능의 작동 방식을 정확하게 설명 하고 데모를 포함하는 JavaScriptDemistifying 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 이벤트가있을 때마다 내부 함수를 호출한다는 것입니다. 모든 크기 조정 이벤트가 배열에 있다고 상상하면 더 분명 할 것입니다.

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다음 반복에 사용할 수 있다는 것을 알 수 있습니까? 그리고 이름 변경에 대한 내 의견으로는, 이유가 없습니다 thiscontentargumentsargs.


"이름 바꾸기"는 반드시 필요합니다. setTimeout () 콜백 함수 의 의미 thisarguments변경 다른 곳에 사본을 보관해야하거나 해당 정보가 손실됩니다.
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

자바 스크립트의 간단한 디 바운스 방법

<!-- 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>

자바 스크립트 :-

    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));
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.