Adakah yang bisa menjelaskan fungsi "debounce" di Javascript


151

Saya tertarik dengan fungsi "debouncing" dalam javascript, yang ditulis di sini: http://davidwalsh.name/javascript-debounce-function

Sayangnya kode ini tidak dijelaskan dengan cukup jelas untuk saya mengerti. Adakah yang bisa membantu saya mencari tahu cara kerjanya (saya meninggalkan komentar saya di bawah). Singkatnya saya hanya benar-benar tidak mengerti cara kerjanya

   // 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: Cuplikan kode yang disalin sebelumnya ada callNowdi tempat yang salah.


1
Jika Anda menelepon clearTimeoutdengan sesuatu yang bukan ID penghitung waktu yang valid, ia tidak melakukan apa-apa.
Ry-

@ salah, Apakah itu perilaku standar yang valid?
Pacerier

3
@Pacerier Ya, itu ada dalam spesifikasi : "Jika pegangan tidak mengidentifikasi entri dalam daftar timer aktif dari WindowTimersobjek di mana metode itu dipanggil, metode tidak melakukan apa-apa."
Mattias Buelens

Jawaban:


134

Kode dalam pertanyaan sedikit diubah dari kode di tautan. Di tautan, ada tanda centang (immediate && !timeout)SEBELUM membuat batas waktu baru. Setelah itu menyebabkan mode langsung tidak pernah menyala. Saya telah memperbarui jawaban saya untuk membubuhi keterangan versi yang berfungsi dari tautan.

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
untuk immediate && timeoutcek. Tidak akan selalu ada timeout(karena timeoutdisebut sebelumnya). Juga, apa gunanya clearTimeout(timeout), ketika dideklarasikan (menjadikannya tidak terdefinisi) dan dihapus, sebelumnya
Startec

The immediate && !timeoutcheck adalah ketika menghilangkan bounce dikonfigurasi dengan immediatebendera. Ini akan menjalankan fungsi segera tetapi memaksakan waitbatas waktu sebelumnya jika dapat dieksekusi lagi. Jadi !timeoutbagian ini pada dasarnya mengatakan 'maaf bub, ini sudah dieksekusi dalam jendela yang ditentukan` ... ingat fungsi setTimeout akan menghapusnya, memungkinkan panggilan berikutnya untuk dieksekusi.
Malk

1
Mengapa batas waktu harus disetel ke nol di dalam setTimeoutfungsi? Juga, saya telah mencoba kode ini, bagi saya, mengirimkan truelangsung hanya mencegah fungsi dipanggil sama sekali (daripada dipanggil setelah penundaan). Apakah ini terjadi untukmu?
Startec

Saya punya pertanyaan serupa tentang langsung? mengapa perlu memiliki param langsung. Pengaturan tunggu ke 0 seharusnya memiliki efek yang sama, bukan? Dan seperti yang disebutkan @Startec, perilaku ini cukup aneh.
zeroliu

2
Jika Anda hanya memanggil fungsi maka Anda tidak bisa memaksakan waktu tunggu sebelum fungsi itu dapat dipanggil lagi. Pikirkan sebuah gim di mana pengguna menutup kunci api. Anda ingin api itu memicu segera, tetapi tidak menembak lagi selama X milidetik lain tidak peduli seberapa cepat pengguna menekan tombol.
Malk

57

Hal penting yang perlu diperhatikan di sini adalah debouncemenghasilkan fungsi yang "ditutup" timeoutvariabel. The timeoutvariabel tetap dapat diakses selama setiap panggilan dari fungsi diproduksi bahkan setelah debounceitu sendiri telah kembali, dan dapat berubah dari panggilan yang berbeda.

Gagasan umum debounceadalah sebagai berikut:

  1. Mulai tanpa batas waktu.
  2. Jika fungsi yang dihasilkan dipanggil, hapus dan setel ulang batas waktu.
  3. Jika batas waktu tercapai, panggil fungsi aslinya.

Poin pertama adalah adil var timeout;, memang adil undefined. Untungnya, clearTimeoutcukup longgar tentang inputnya: melewatkan undefinedpengenal waktu menyebabkannya tidak melakukan apa-apa, itu tidak menimbulkan kesalahan atau sesuatu.

Poin kedua dilakukan oleh fungsi yang diproduksi. Pertama-tama menyimpan beberapa informasi tentang panggilan ( thiskonteks dan arguments) dalam variabel sehingga nantinya dapat menggunakan ini untuk panggilan yang dibatalkan. Ini kemudian menghapus batas waktu (jika ada satu set) dan kemudian membuat yang baru untuk menggantinya menggunakan setTimeout. Perhatikan bahwa ini menimpa nilai timeoutdan nilai ini bertahan selama beberapa panggilan fungsi! Ini memungkinkan debounce benar-benar berfungsi: jika fungsinya disebut berkali-kali, timeoutditimpa beberapa kali dengan timer baru. Jika ini tidak terjadi, beberapa panggilan akan menyebabkan beberapa timer dimulai yang semuanya tetap aktif - panggilan hanya akan ditunda, tetapi tidak ditolak.

Poin ketiga dilakukan dalam callback timeout. Ini menghapus timeoutvariabel dan melakukan panggilan fungsi sebenarnya menggunakan informasi panggilan yang disimpan.

The immediatebendera seharusnya mengontrol apakah fungsi harus dipanggil sebelum atau setelah timer. Jika ya false, fungsi asli tidak dipanggil hingga setelah penghitung waktu diaktifkan. Jika ya true, fungsi asli pertama kali dipanggil dan tidak akan dipanggil lagi sampai timer dihidupkan.

Namun, saya percaya bahwa if (immediate && !timeout)cek yang salah: timeoutbaru saja siap untuk timer identifier dikembalikan oleh setTimeoutbegitu !timeoutselalu falsepada saat itu dan dengan demikian fungsi tidak bisa disebut. Versi underscore.js saat ini tampaknya memiliki pemeriksaan yang sedikit berbeda, di mana ia mengevaluasi immediate && !timeout sebelum menelepon setTimeout. (Algoritma ini juga sedikit berbeda, misalnya tidak digunakan clearTimeout.) Itu sebabnya Anda harus selalu mencoba menggunakan versi terbaru perpustakaan Anda. :-)


"Perhatikan bahwa ini menimpa nilai batas waktu dan nilai ini tetap ada pada beberapa panggilan fungsi" Bukankah batas waktu lokal untuk setiap panggilan debounce? Ini dideklarasikan dengan var. Bagaimana itu ditimpa setiap kali? Juga, mengapa memeriksa !timeoutdi akhir? Mengapa tidak selalu ada (karena diatur kesetTimeout(function() etc.)
Startec

2
@Startec Ini lokal untuk setiap panggilan debounce, ya, tetapi dibagikan di antara panggilan ke fungsi yang dikembalikan (yang merupakan fungsi yang akan Anda gunakan). Misalnya, dalam g = debounce(f, 100), nilai timeouttetap dari beberapa panggilan g. The !timeoutcek di akhir adalah kesalahan saya percaya, dan tidak dalam kode underscore.js saat ini.
Mattias Buelens

Mengapa batas waktu harus dihapus lebih awal dalam fungsi pengembalian (tepat setelah dinyatakan)? Juga, ini kemudian diatur ke nol di dalam fungsi setTimeout. Bukankah ini berlebihan? (Pertama dihapus, lalu diatur ke null. Dalam pengujian saya dengan kode di atas, pengaturan langsung ke true membuat fungsi tidak memanggil sama sekali, seperti yang Anda sebutkan. Ada solusi tanpa garis bawah?
Startec

34

Fungsi-fungsi yang di-debug tidak dieksekusi ketika dipanggil, mereka menunggu jeda doa selama durasi yang dapat dikonfigurasi sebelum mengeksekusi; setiap doa baru me-restart timer.

Fungsi-fungsi yang diperlambat menjalankan dan kemudian menunggu durasi yang dapat dikonfigurasi sebelum memenuhi syarat untuk memecat lagi.

Debounce sangat bagus untuk acara penekanan tombol; ketika pengguna mulai mengetik dan kemudian berhenti Anda mengirim semua penekanan tombol sebagai peristiwa tunggal, sehingga mengurangi doa penanganan.

Throttle sangat bagus untuk titik akhir waktu nyata yang Anda hanya ingin memungkinkan pengguna untuk memanggil sekali per periode waktu yang ditentukan.

Lihat juga Underscore.js untuk implementasinya.


24

Saya menulis posting berjudul Demistifying Debounce di JavaScript di mana saya menjelaskan dengan tepat bagaimana fungsi debounce bekerja dan menyertakan demo.

Saya juga tidak sepenuhnya mengerti bagaimana fungsi debounce bekerja ketika saya pertama kali menemukannya. Meskipun ukurannya relatif kecil, mereka sebenarnya menggunakan beberapa konsep JavaScript yang cukup canggih! Memiliki pegangan yang baik pada ruang lingkup, penutupan dan setTimeoutmetode akan membantu.

Dengan itu, di bawah ini adalah fungsi dasar debounce yang dijelaskan dan didemokan di posting saya yang dirujuk di atas.

Produk jadi

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

Penjelasan

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

Yang ingin Anda lakukan adalah sebagai berikut: Jika Anda mencoba memanggil fungsi secara berurutan, yang pertama harus dibatalkan dan yang baru harus menunggu batas waktu yang diberikan dan kemudian jalankan. Jadi, Anda perlu beberapa cara untuk membatalkan batas waktu dari fungsi pertama? Tapi bagaimana caranya? Anda bisa memanggil fungsi tersebut, dan meneruskan id timeout-kembali dan kemudian memberikan ID itu ke fungsi baru. Tetapi solusi di atas jauh lebih elegan.

Apa yang dilakukannya secara efektif membuat timeoutvariabel tersedia dalam lingkup fungsi yang dikembalikan. Jadi ketika acara 'resize' dipecat, ia tidak menelepon debounce()lagi, maka timeoutisinya tidak berubah (!) Dan masih tersedia untuk "panggilan fungsi selanjutnya".

Kuncinya di sini adalah pada dasarnya bahwa kita memanggil fungsi internal setiap kali kita memiliki ukuran peristiwa. Mungkin lebih jelas jika kita membayangkan semua peristiwa-ukuran dalam array:

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

Anda melihat timeouttersedia untuk iterasi berikutnya? Dan tidak ada alasan, menurut saya untuk mengubah nama thisuntuk contentdan argumentsuntuk args.


"Mengganti nama" mutlak diperlukan. Arti thisdan argumentsperubahan di dalam fungsi callback setTimeout (). Anda harus menyimpan salinan di tempat lain atau info itu hilang.
CubicleSoft

1

Ini adalah variasi yang selalu mengaktifkan fungsi yang didebok saat pertama kali dipanggil, dengan variabel yang lebih deskriptif bernama:

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

Metode Debounce sederhana dalam 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))

Runtime Example JSFiddle: https://jsfiddle.net/arbaazshaikh919/d7543wqe/10/


0

Fungsi debounce sederhana: -

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));
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.