Birisi Javascript'te "geri dönme" işlevini açıklayabilir


151

Javascript, burada yazılı "debouncing" fonksiyonu ile ilgileniyorum: http://davidwalsh.name/javascript-debounce-function

Ne yazık ki kod anlayabileceğim kadar açık bir şekilde açıklanmadı. Herkes nasıl çalıştığını anlamama yardımcı olabilir (Ben aşağıda yorum bıraktı). Kısacası bunun nasıl çalıştığını gerçekten anlamıyorum

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

DÜZENLEME: Kopyalanan kod snippet'inde önceden callNowyanlış nokta vardı .


1
clearTimeoutGeçerli bir zamanlayıcı kimliği olmayan bir şeyle ararsanız hiçbir şey yapmaz.
Ry-

@false, Bu geçerli standart davranış mı?
Pacerier

3
@Pacerier Evet, spesifikasyonda : "Tanıtıcı WindowTimers, yöntemin çağrıldığı nesnenin etkin zamanlayıcıları listesindeki bir girdiyi tanımlamazsa , yöntem hiçbir şey yapmaz."
Mattias Buelens

Yanıtlar:


134

Söz konusu kod, bağlantıdaki koddan biraz değiştirildi. Bağlantıda, (immediate && !timeout)yeni bir zaman aşımı oluşturmadan ÖNCE bir kontrol var . Sonra sahip olmak anında modun asla ateş etmemesine neden olur. Bağlantıdan çalışan sürüme açıklama eklemek için cevabımı güncelledim.

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
için immediate && timeoutçek. Her zaman bir olmayacak mı timeout(çünkü timeoutdaha önce denir). Ayrıca, daha önce ne iyi yapılır, ne clearTimeout(timeout)zaman tanımlanırsa (tanımsız hale getirilir) ve temizlenir, daha önce
Startec

immediate && !timeoutÇek filtreleme ile yapılandırıldığında içindir immediatebayrak. Bu, işlevi hemen yürütür, ancak waittekrar çalıştırılabilirse daha önce bir zaman aşımı uygular . Yani !timeoutbölüm temelde 'özür dilerim bub, bu zaten tanımlanmış pencerede yürütüldü` diyor ... setTimeout işlevinin onu temizleyeceğini ve bir sonraki çağrının yürütülmesine izin vereceğini unutmayın.
Malk

1
Zaman aşımı neden setTimeoutişlevin içinde null değerine ayarlanmalıdır ? Ayrıca, bu kodu denedim, benim trueiçin, hemen geçerek sadece fonksiyonun çağrılmasını önler (bir gecikmeden sonra çağrılmak yerine). Bu senin için mi oluyor?
Startec

Hemen hakkında benzer bir sorum mu var? neden hemen param olması gerekiyor. 0 değerine bekleme ayarı aynı etkiye sahip olmalı, değil mi? @Startec'in de belirttiği gibi, bu davranış oldukça garip.
zeroliu

2
Sadece fonksiyonu çağırırsanız, o fonksiyon tekrar aranmadan önce bir bekleme zamanlayıcısı uygulayamazsınız. Kullanıcının yangın anahtarını ezdiği bir oyun düşünün. Bu yangının hemen tetiklenmesini istiyorsunuz, ancak kullanıcı düğmeyi ne kadar hızlı ezerse sıkıştırsın, başka bir X milisaniye için tekrar ateş etmeyin.
Malk

57

Burada dikkat edilmesi gereken önemli nokta , değişkenin "kapalı" olduğu debouncebir işlev üretmesidir timeout. timeoutSonra bile üretilen fonksiyonun her arama sırasında erişilebilir değişken kalır debouncekendisi döndü, ve olabilir farklı aramaları içinde değişebilir.

Genel fikir debounceşudur:

  1. Zaman aşımı olmadan başlayın.
  2. Üretilen işlev çağrılırsa, zaman aşımını temizleyin ve sıfırlayın.
  3. Zaman aşımı gerçekleşirse, orijinal işlevi çağırın.

İlk nokta sadece var timeout;, gerçekten sadece undefined. Neyse ki, clearTimeoutgirdisi konusunda oldukça gevşek: bir undefinedzamanlayıcı tanımlayıcısının geçmesi , hiçbir şey yapmamasına neden oluyor, bir hata ya da bir şey atmıyor.

İkinci nokta üretilen fonksiyon tarafından yapılır. Öncelikle çağrı ( thisbağlam ve arguments) ile ilgili bazı bilgileri değişkenlerde saklar, böylece daha sonra bunları geri çevrilen çağrı için kullanabilir. Daha sonra zaman aşımını (bir küme varsa) temizler ve ardından kullanarak değiştirmek için yeni bir zaman oluşturur setTimeout. Bunun değerinin üzerine yazıldığını timeoutve bu değerin birden çok işlev çağrısında devam ettiğini unutmayın ! Bu, başlatmanın gerçekten çalışmasına izin verir: işlev birden çok kez çağrılırsa, timeoutyeni bir zamanlayıcı ile birden çok kez yazılır. Eğer durum böyle değilse, birden fazla çağrı tümünün etkin kalması için çoklu zamanlayıcıların başlatılmasına neden olur - çağrılar sadece ertelenir, ancak iptal edilmez.

Üçüncü nokta zaman aşımı geri çağrısında yapılır. Değişkeni sıfırlar ve timeoutsaklanan çağrı bilgilerini kullanarak gerçek fonksiyon çağrısını yapar.

immediateBayrak işlevi denilen gerekip gerekmediğini kontrol etmek gerekiyordu önce veya sonra zamanlayıcı. Eğer durum bu ise false, orijinal işlevi kadar çağrılmaz sonra zamanlayıcı vuruldu. Eğer öyleyse, trueorijinal işlev önce çağrılır ve zamanlayıcı vuruluncaya kadar artık çağrılmaz.

Ancak, ben if (immediate && !timeout)kontrol yanlış olduğuna inanıyorum : timeoutsadece zamanlayıcı tanımlayıcı olarak ayarlanmış setTimeoutbu yüzden !timeouther zaman falsebu noktada ve böylece fonksiyon asla çağrılamaz. Underscore.js dosyasının şu anki sürümü biraz farklı bir kontrole sahip gibi görünüyor ve burada aramadan immediate && !timeout önce değerlendiriliyor setTimeout. (Algoritma da biraz farklıdır, örneğin kullanmaz clearTimeout.) Bu yüzden her zaman kütüphanelerinizin en son sürümünü kullanmaya çalışmalısınız. :-)


"Bu, zaman aşımı değerinin üzerine yazıldığını ve bu değerin birden çok işlev çağrısında devam ettiğini unutmayın" Zaman aşımı her geri dönen çağrı için yerel değil mi? Var. Her seferinde nasıl yazılır? Ayrıca, neden !timeoutsonunda kontrol etmeliyim? Neden her zaman mevcut değil (çünkü şu şekilde ayarlanmıştırsetTimeout(function() etc.)
Startec

2
@Startec debounceEvet, her çağrısında yereldir , ancak döndürülen işleve (kullanacağınız işlevdir) yapılan çağrılar arasında paylaşılır . Örneğin, içinde g = debounce(f, 100), değeri timeoutbirden çok çağrısında devam eder g. !timeoutSonunda onay inanıyorum bir hatadır ve mevcut underscore.js kodunda değildir.
Mattias Buelens

Zaman aşımı neden geri dönüş fonksiyonunda erken belirtilmelidir (bildirildikten hemen sonra)? Ayrıca, setTimeout işlevinin içinde null değerine ayarlanır. Bu gereksiz değil mi? (Önce temizlenir, sonra ayarlanır null. Yukarıdaki kodla yaptığım testlerde, hemen true değerine ayarlanması, belirttiğiniz gibi işlevi hiç çağırmaz. Alt çizgi olmadan herhangi bir çözüm?
Startec

34

Kaldırılmış işlevler çağrıldığında çalışmaz, yürütülmeden önce yapılandırılabilir bir süre boyunca çağrıların duraklatılmasını bekler; her yeni çağrı zamanlayıcıyı yeniden başlatır.

Kısılmış işlevler yürütülür ve yeniden ateşlenmeye uygun hale gelmeden önce yapılandırılabilir bir süre bekler.

Debounce tuşa basma olayları için mükemmeldir; kullanıcı yazmaya başladığında ve ardından duraklattığında tüm tuşa basma işlemlerini tek bir olay olarak gönderdiğinizde işlem çağrılarını azaltırsınız.

Gaz, kullanıcının yalnızca belirli bir süre için bir kez çağırmasına izin vermek istediğiniz gerçek zamanlı uç noktaları için mükemmeldir.

Uygulamaları için Underscore.js'ye de göz atın .


25

JavaScript'te Debounce'u Demistifying adlı bir yazı yazdım, burada bir debounce işlevinin nasıl çalıştığını tam olarak açıklarım ve bir demo eklerim .

Ben de bir tanışma fonksiyonu ilk karşılaştığımda nasıl çalıştığını tam olarak anlamadım. Nispeten küçük olmasına rağmen, aslında oldukça gelişmiş bazı JavaScript kavramları kullanırlar! Kapsamı, kapanışları ve setTimeoutyöntemi iyi kavramak yardımcı olacaktır.

Bununla birlikte, aşağıda atıfta bulunulan yazımda açıklanan ve açıklamanın temel açılma fonksiyonu aşağıdadır.

Bitmiş ürün

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

Açıklama

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

Yapmak istediğiniz şey şudur: Bir işlevi birbiri ardına çağırmaya çalışırsanız, birincisi iptal edilmeli ve yenisi belirli bir zaman aşımını beklemeli ve çalıştırmalıdır. Yani aslında ilk işlevin zaman aşımını iptal etmenin bir yolunu mu arıyorsunuz? Ama nasıl? Sen olabilir işlevini çağırın ve dönen zaman aşımı-id geçmek ve daha sonra herhangi bir yeni fonksiyonlar içine bu kimliği geçmektedir. Ancak yukarıdaki çözüm çok daha zariftir.

Yaptığı şey, timeoutdeğişkeni döndürülen işlev kapsamında kullanılabilir hale getirmektir . Bir 'resize' olayı tetiklendiğinde debounce()tekrar çağrılmaz , bu nedenle timeoutiçerik değiştirilmez (!) Ve hala "sonraki işlev çağrısı" için kullanılabilir.

Buradaki anahtar şey, temelde her yeniden boyutlandırma olayı yaptığımızda dahili işlevi çağırmamızdır. Belki de tüm resize olaylarının bir dizide olduğunu hayal edersek daha açıktır:

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

Bir timeoutsonraki yinelemede kullanılabilir olduğunu görüyor musunuz? Ve yeniden adlandırma bence, hiçbir neden yoktur thisiçin contentve argumentsiçin args.


"Yeniden adlandırma" kesinlikle gereklidir. SetTimeout () geri arama işlevinin anlamı thisve argumentsiçindeki değişiklikler. Başka bir yerde bir kopya bulundurmanız gerekiyor veya bu bilgi kayboluyor.
CubicleSoft

1

Bu, tanımlayıcı olarak adlandırılmış değişkenlerle, ilk çağrıldığında her zaman kaldırılmış işlevi başlatan bir varyasyondur:

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'te basit Debounce yöntemi

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

Çalışma Zamanı Örneği JSFiddle: https://jsfiddle.net/arbaazshaikh919/d7543wqe/10/


0

Basit açılma fonksiyonu: -

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));
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.