誰かがJavaScriptの「デバウンス」機能を説明できますか


151

ここに書かれた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、それは有効な標準動作ですか?
パーチェリエ2014年

3
@Pacerierはい、それは仕様にあります。「ハンドルがWindowTimers、メソッドが呼び出されたオブジェクトのアクティブタイマーのリストのエントリを識別しない場合、メソッドは何もしません。」
Mattias Buelens 14年

回答:


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チェックのため。が常に存在するわけではありませんtimeouttimeout以前に呼び出されるため)。また、clearTimeout(timeout)それが宣言されて(未定義にされて)クリアされたときに、何が良いのか
Startec

このimmediate && !timeoutチェックは、デバウンスがimmediateフラグで構成されている場合に行われます。これにより、関数はすぐwaitに実行されますが、再度実行できる場合は、タイムアウトが発生します。したがって、この!timeout部分は基本的に「申し訳ありません、これは定義済みのウィンドウ内で既に実行されました」と言っています... setTimeout関数がそれをクリアし、次の呼び出しの実行を許可することに注意してください。
マルク、2014年

1
setTimeout関数内でタイムアウトをnullに設定する必要があるのはなぜですか?また、私はこのコードを試しましたがtrue、即時に渡すと、(遅延後に呼び出されるのではなく)関数がまったく呼び出されなくなります。これはあなたのために起こりますか?
Startec

即時について同様の質問がありますか?なぜ即時パラメータが必要なのですか?waitを0に設定しても同じ効果がありますよね?そして、@ Startecが述べたように、この動作はかなり奇妙です。
zeroliu 2015

2
関数を呼び出すだけの場合、その関数を再度呼び出す前に待機タイマーを設定することはできません。ユーザーが発射キーをマッシュするゲームを考えてください。その発砲をすぐにトリガーしたいが、ユーザーがボタンを押しつぶす速さに関係なく、さらにXミリ秒間再び発砲しないようにしたい場合。
Malk 2015

57

ここで注意すべき重要なことは、変数を「閉じた」関数debounce生成することです。でも後に生成関数のすべての呼び出し中にアクセス変数滞在自体が戻ってきた、とすることができます別の通話を切り替えます。timeouttimeoutdebounce

の一般的な考え方debounceは次のとおりです。

  1. タイムアウトなしで開始します。
  2. 生成された関数が呼び出された場合は、タイムアウトをクリアしてリセットします。
  3. タイムアウトに達した場合は、元の関数を呼び出します。

最初のポイントはちょうどですvar timeout;、それは確かにちょうどundefinedです。幸いなことに、clearTimeoutその入力はかなり緩やかです。undefinedタイマー識別子を渡しても、何も実行されず、エラーや何かがスローされることはありません。

2番目のポイントは、生成された関数によって行われます。最初に、呼び出しに関するいくつかの情報(thisコンテキストとarguments)を変数に格納するので、後でデバウンスされた呼び出しにこれらを使用できます。次に、タイムアウトをクリアし(1つのセットが存在する場合)、新しいタイムアウトを作成して、を使用してそれを置き換えsetTimeoutます。これはの値を上書きし、timeoutこの値は複数の関数呼び出しにわたって持続することに注意してください!これにより、デバウンスが実際に機能するようになります。関数が複数回呼び出された場合、timeout新しいタイマーで複数回上書きされます。これが当てはまらない場合、複数の呼び出しにより複数のタイマーが開始され、すべてがアクティブのままになります-呼び出しは単に遅延されますが、デバウンスされません。

3番目のポイントは、タイムアウトコールバックで行われます。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 14年

なぜreturn関数の早い段階で(宣言された直後に)タイムアウトをクリアする必要があるのですか?また、setTimeout関数内でnullに設定されます。これは冗長ではありませんか?(最初にクリアされ、次にに設定されnullます。上記のコードを使用したテストでは、即時にtrueに設定すると、前述のように関数がまったく呼び出されなくなります。アンダースコアのない解決策はありますか?
Startec

34

デバウンスされた関数は、呼び出されても実行されず、構成可能な期間にわたって呼び出しの一時停止を待ってから実行されます。新しい呼び出しごとにタイマーが再起動します。

スロットルされた関数が実行され、構成可能な期間待機してから、再度起動できます。

デバウンスはキープレスイベントに最適です。ユーザーが入力を開始してから一時停止すると、すべてのキープレスが単一のイベントとして送信され、呼び出しの処理が削減されます。

スロットルは、ユーザーが一定期間に1回だけ呼び出すことを許可するリアルタイムエンドポイントに最適です。

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-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次のイテレーションで使用できるのがわかりますか?名前の変更に私の意見では、理由がないthiscontentargumentsしますargs


「名前の変更」は絶対に必要です。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

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));
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.