Ai đó có thể giải thích chức năng gỡ lỗi của Wikipedia trong Javascript không


151

Tôi quan tâm đến chức năng "gỡ lỗi" trong javascript, được viết tại đây: http://davidwalsh.name/javascript-debounce-feft

Thật không may, mã không được giải thích rõ ràng đủ để tôi hiểu. Bất cứ ai có thể giúp tôi tìm ra cách nó hoạt động (tôi để lại ý kiến ​​của tôi dưới đây). Tóm lại tôi thực sự không hiểu làm thế nào điều này hoạt động

   // 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: Đoạn mã được sao chép trước đây đã callNowở sai vị trí.


1
Nếu bạn gọi clearTimeoutvới một cái gì đó không phải là ID hẹn giờ hợp lệ, nó sẽ không làm gì cả.
Ry-

@false, Đó có phải là hành vi tiêu chuẩn hợp lệ không?
Pacerier

3
@Pacerier Có, nó nằm trong thông số : "Nếu xử lý không xác định một mục trong danh sách các bộ định thời hoạt động của WindowTimersđối tượng mà phương thức được gọi, phương thức không làm gì cả."
Mattias Buelens

Câu trả lời:


134

Mã trong câu hỏi đã được thay đổi một chút từ mã trong liên kết. Trong liên kết, có một kiểm tra (immediate && !timeout)TRƯỚC KHI tạo thời gian chờ mới. Có nó sau khi gây ra chế độ ngay lập tức để không bao giờ bắn. Tôi đã cập nhật câu trả lời của mình để chú thích phiên bản làm việc từ liên kết.

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 && timeoutkiểm tra Sẽ không luôn luôn có một timeout(vì timeoutđược gọi sớm hơn). Ngoài ra, những gì tốt clearTimeout(timeout)làm, khi nó được tuyên bố (làm cho nó không được xác định) và bị xóa, trước đó
Startec

Các immediate && !timeoutkiểm tra là khi debounce được cấu hình với các immediatelá cờ. Điều này sẽ thực thi chức năng ngay lập tức nhưng áp đặt waitthời gian chờ trước nếu có thể được thực hiện lại. Vì vậy, !timeoutphần cơ bản là nói 'xin lỗi bong bóng, điều này đã được thực hiện trong cửa sổ được xác định` ... hãy nhớ rằng hàm setTimeout sẽ xóa nó, cho phép thực hiện cuộc gọi tiếp theo.
Malk

1
Tại sao thời gian chờ phải được đặt thành null bên trong setTimeouthàm? Ngoài ra, tôi đã thử mã này, đối với tôi, trueviệc truy cập ngay lập tức chỉ ngăn chức năng được gọi ở tất cả (thay vì được gọi sau khi trì hoãn). Điều này xảy ra cho bạn?
Bắt đầu

Tôi có một câu hỏi tương tự về ngay lập tức? Tại sao nó cần phải có param ngay lập tức. Đặt chờ thành 0 sẽ có tác dụng tương tự, phải không? Và như @Startec đã đề cập, hành vi này khá kỳ lạ.
zeroliu

2
Nếu bạn chỉ gọi hàm thì bạn không thể đặt thời gian chờ trước khi chức năng đó có thể được gọi lại. Hãy nghĩ về một trò chơi mà người dùng nghiền phím lửa. Bạn muốn ngọn lửa đó kích hoạt ngay lập tức, nhưng không kích hoạt lại trong một mili giây nữa cho dù người dùng có nhấn nút nhanh như thế nào.
Malk

57

Điều quan trọng cần lưu ý ở đây là debouncetạo ra một hàm "đóng trên" timeoutbiến. Các timeouttrú biến truy cập trong mỗi cuộc gọi của hàm sản xuất ngay cả sau khi debouncebản thân đã trở lại, và có thể thay đổi theo cuộc gọi khác nhau.

Ý tưởng chung cho debouncelà như sau:

  1. Bắt đầu không có thời gian chờ.
  2. Nếu chức năng sản xuất được gọi, xóa và đặt lại thời gian chờ.
  3. Nếu hết thời gian, hãy gọi chức năng ban đầu.

Điểm đầu tiên là var timeout;, nó thực sự chỉ là undefined. May mắn thay, clearTimeoutkhá lỏng lẻo về đầu vào của nó: việc chuyển một undefinedđịnh danh hẹn giờ khiến nó không làm gì cả, nó không gây ra lỗi hay gì.

Điểm thứ hai được thực hiện bởi hàm sản xuất. Đầu tiên, nó lưu trữ một số thông tin về cuộc gọi ( thisbối cảnh và arguments) trong các biến để sau này có thể sử dụng chúng cho cuộc gọi bị từ chối. Sau đó, nó sẽ xóa thời gian chờ (nếu có một bộ) và sau đó tạo một bộ mới để thay thế nó bằng cách sử dụng setTimeout. Lưu ý rằng điều này ghi đè lên giá trị timeoutvà giá trị này vẫn tồn tại qua nhiều lệnh gọi hàm! Điều này cho phép gỡ lỗi thực sự hoạt động: nếu chức năng được gọi nhiều lần, timeoutđược ghi đè nhiều lần với bộ đếm thời gian mới. Nếu đây không phải là trường hợp, nhiều cuộc gọi sẽ khiến nhiều bộ định thời được bắt đầu mà tất cả vẫn hoạt động - các cuộc gọi đơn giản sẽ bị trì hoãn, nhưng không được công bố.

Điểm thứ ba được thực hiện trong cuộc gọi lại thời gian chờ. Nó bỏ đặt timeoutbiến và thực hiện cuộc gọi chức năng thực tế bằng cách sử dụng thông tin cuộc gọi được lưu trữ.

Các immediatelá cờ có nghĩa vụ phải kiểm soát xem các chức năng nên được gọi là trước hay sau khi bộ đếm thời gian. Nếu có false, chức năng ban đầu không được gọi cho đến khi bộ đếm thời gian được nhấn. Nếu đúng như vậy true, chức năng ban đầu được gọi đầu tiên và sẽ không được gọi nữa cho đến khi bộ đếm thời gian được nhấn.

Tuy nhiên, tôi tin rằng if (immediate && !timeout)kiểm tra là sai: timeoutvừa được đặt thành định danh bộ đếm thời gian được trả về bởi setTimeoutvì vậy !timeoutluôn luôn falseở điểm đó và do đó chức năng không bao giờ có thể được gọi. Phiên bản hiện tại của underscore.js dường như có một kiểm tra hơi khác, nơi nó đánh giá immediate && !timeout trước khi gọi setTimeout. (Thuật toán cũng hơi khác một chút, ví dụ: nó không sử dụng clearTimeout.) Đó là lý do tại sao bạn nên luôn cố gắng sử dụng phiên bản mới nhất của thư viện. :-)


"Lưu ý rằng điều này ghi đè lên giá trị của thời gian chờ và giá trị này vẫn tồn tại qua nhiều lệnh gọi" Không hết thời gian cục bộ cho mỗi cuộc gọi gỡ lỗi? Nó được khai báo với var. Làm thế nào nó được ghi đè mỗi lần? Ngoài ra, tại sao kiểm tra !timeoutvào cuối? Tại sao nó không tồn tại (vì nó được đặt thànhsetTimeout(function() etc.)
Startec

2
@Startec Nó là cục bộ cho mỗi cuộc gọi của debounce, vâng, nhưng nó được chia sẻ giữa các cuộc gọi đến hàm trả về (đây là chức năng bạn sẽ sử dụng). Ví dụ: trong g = debounce(f, 100), giá trị của timeoutvẫn tồn tại qua nhiều cuộc gọi đến g. Các !timeoutkiểm tra ở cuối là một sai lầm Tôi tin, và nó không phải là trong mã underscore.js hiện hành.
Mattias Buelens

Tại sao thời gian chờ cần phải được xóa sớm trong hàm trả về (ngay sau khi được khai báo)? Ngoài ra, sau đó nó được đặt thành null bên trong hàm setTimeout. Đây không phải là dư thừa? (Đầu tiên nó sẽ bị xóa, sau đó nó được thiết lập để nullTrong các thử nghiệm của tôi với các mã trên, thiết lập ngay lập tức để làm cho đúng chức năng không gọi ở tất cả, như bạn nói Bất kỳ giải pháp mà không gạch dưới..?
STARTEC

34

Các hàm bị lỗi không thực thi khi được gọi, chúng chờ tạm dừng các lệnh trong khoảng thời gian có thể định cấu hình trước khi thực hiện; mỗi lần gọi mới khởi động lại bộ đếm thời gian.

Các chức năng điều chỉnh thực thi và sau đó đợi một khoảng thời gian có thể định cấu hình trước khi đủ điều kiện để kích hoạt lại.

Debounce là tuyệt vời cho các sự kiện nhấn phím; khi người dùng bắt đầu nhập và sau đó tạm dừng, bạn gửi tất cả các lần nhấn phím dưới dạng một sự kiện duy nhất, do đó cắt giảm các yêu cầu xử lý.

Van tiết lưu rất tốt cho các điểm cuối thời gian thực mà bạn chỉ muốn cho phép người dùng gọi một lần trong một khoảng thời gian đã đặt.

Hãy xem Underscore.js để biết cách triển khai của họ.


24

Tôi đã viết một bài đăng có tiêu đề Demistifying Debounce trong JavaScript nơi tôi giải thích chính xác cách thức hoạt động của chức năng gỡ lỗi và bao gồm một bản demo.

Tôi cũng không hiểu đầy đủ chức năng gỡ lỗi hoạt động như thế nào khi tôi gặp lần đầu tiên. Mặc dù kích thước tương đối nhỏ, họ thực sự sử dụng một số khái niệm JavaScript khá tiên tiến! Có một nắm bắt tốt về phạm vi, đóng cửa và setTimeoutphương pháp sẽ giúp.

Như đã nói, bên dưới là chức năng gỡ lỗi cơ bản được giải thích và demo trong bài viết của tôi được tham chiếu ở trên.

Thành phẩm

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

Lời giải thích

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

Những gì bạn muốn làm là như sau: Nếu bạn cố gắng gọi một hàm ngay sau một hàm khác, hàm đầu tiên sẽ bị hủy và hàm mới sẽ đợi trong một khoảng thời gian chờ nhất định và sau đó thực thi. Vì vậy, trong thực tế, bạn cần một số cách để hủy thời gian chờ của chức năng đầu tiên? Nhưng bằng cách nào? Bạn có thể gọi hàm và chuyển id thời gian chờ trả về và sau đó chuyển ID đó vào bất kỳ hàm mới nào. Nhưng giải pháp ở trên là cách thanh lịch hơn.

Những gì nó làm là có hiệu quả làm cho timeoutbiến có sẵn trong phạm vi của hàm trả về. Vì vậy, khi một sự kiện 'thay đổi kích thước' được kích hoạt, nó sẽ không gọi debounce()lại, do đó timeoutnội dung không bị thay đổi (!) Và vẫn có sẵn cho "cuộc gọi chức năng tiếp theo".

Điều quan trọng ở đây là về cơ bản là chúng ta gọi hàm nội bộ mỗi khi chúng ta có một sự kiện thay đổi kích thước. Có lẽ rõ ràng hơn nếu chúng ta tưởng tượng tất cả thay đổi kích thước - các sự kiện nằm trong một mảng:

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

Bạn thấy timeoutcó sẵn để lặp lại tiếp theo? Và không có lý do, theo tôi để đổi tên thisthành contentargumentssang args.


"Đổi tên" là hoàn toàn cần thiết. Ý nghĩa thisargumentsthay đổi bên trong hàm gọi lại setTimeout (). Bạn phải giữ một bản sao ở nơi khác hoặc thông tin đó bị mất.
CubicleSoft

1

Đây là một biến thể luôn kích hoạt hàm bị gỡ lỗi trong lần đầu tiên được gọi, với các biến được mô tả nhiều hơn:

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

Phương pháp gỡ lỗi đơn giản trong 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))

Ví dụ về thời gian chạy JSFiddle: https://jsfiddle.net/arbaazshaikh919/d7543wqe/10/


0

Chức năng gỡ lỗi đơn giản: -

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));
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.