簡體   English   中英

JavaScript中的“去抖動”function是什么?

[英]What is the "debounce" function in JavaScript?

我對 JavaScript 中的“去抖動”function 感興趣,在JavaScript 去抖動 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

問題中的代碼與鏈接中的代碼略有不同。 在鏈接中,在創建新的超時之前檢查(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(ex, ey); } // Define the debounced function var debouncedMouseMove = debounce(onMouseMove, 50); // Call the debounced function on every mouse move window.addEventListener('mousemove', debouncedMouseMove);

這里要注意的重要一點是, debounce會產生一個“關閉” timeout變量的函數 即使在debounce本身返回之后, timeout變量在每次調用生成的函數期間仍然可以訪問,並且可以在不同的調用中進行更改。

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似乎有一個稍微不同的檢查,它在調用setTimeout之前immediate && !timeout (算法也有點不同,例如它不使用clearTimeout 。)這就是為什么您應該始終嘗試使用最新版本的庫。 :-)

去抖函數在調用時不會執行,它們在執行前等待調用暫停超過可配置的持續時間; 每次新的調用都會重新啟動計時器。

受限制的函數會執行,然后等待一段可配置的持續時間,然后才能再次觸發。

Debounce 非常適合按鍵事件; 當用戶開始輸入然后暫停時,您將所有按鍵作為單個事件提交,從而減少了處理調用。

Throttle 非常適合您只希望允許用戶在設定的時間段內調用一次的實時端點。

也可以查看Underscore.js的實現。

我寫了一篇題為Demistifying Debounce in JavaScript的文章,我在其中准確解釋了 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);
        }
    };
};

我們現在都在使用 Promise

我見過的許多實現使問題過於復雜或存在其他衛生問題。 現在是 2021 年,我們已經使用 Promises 很長時間了——而且有充分的理由。 Promise 清理異步程序並減少發生錯誤的機會。 在這篇文章中,我們將編寫自己的debounce 此實施將 -

  • 在任何給定時間最多有一個待處理的承諾(每個去抖任務)
  • 通過正確取消未決的承諾來阻止內存泄漏
  • 僅解決最新的承諾
  • 通過實時代碼演示演示正確的行為

我們用它的兩個參數編寫debounce ,要 debounce 的task和要延遲的毫秒數ms 我們為其本地狀態引入單個本地綁定, t -

function debounce (task, ms) {
  let t = { promise: null, cancel: _ => void 0 }
  return async (...args) => {
    try {
      t.cancel()
      t = deferred(ms)
      await t.promise
      await task(...args)
    }
    catch (_) { /* prevent memory leak */ }
  }
}

我們依賴於一個可重用的deferred函數,它創建了一個在ms毫秒內解析的新 Promise。 它引入了兩個本地綁定, promise本身,以及cancel它的能力 -

function deferred (ms) {
  let cancel, promise = new Promise((resolve, reject) => {
    cancel = reject
    setTimeout(resolve, ms)
  })
  return { promise, cancel }
}

點擊計數器示例

在第一個示例中,我們有一個統計用戶點擊次數的按鈕。 事件偵聽器使用debounce附加,因此計數器僅在指定持續時間后遞增 -

 // debounce, deferred function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } } function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } } // dom references const myform = document.forms.myform const mycounter = myform.mycounter // event handler function clickCounter (event) { mycounter.value = Number(mycounter.value) + 1 } // debounced listener myform.myclicker.addEventListener("click", debounce(clickCounter, 1000))
 <form id="myform"> <input name="myclicker" type="button" value="click" /> <output name="mycounter">0</output> </form>

實時查詢示例,“自動完成”

在第二個示例中,我們有一個帶有文本輸入的表單。 我們的search查詢是使用debounce附加的 -

 // debounce, deferred function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } } function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } } // dom references const myform = document.forms.myform const myresult = myform.myresult // event handler function search (event) { myresult.value = `Searching for: ${event.target.value}` } // debounced listener myform.myquery.addEventListener("keypress", debounce(search, 1000))
 <form id="myform"> <input name="myquery" placeholder="Enter a query..." /> <output name="myresult"></output> </form>

多次去抖動,react hook useDebounce

另一個問答中,有人問是否可以使用暴露去抖動取消機制並創建一個useDebounce React 鈎子。 使用上面的deferred ,這是一個簡單的練習。

// revised implementation
function debounce(task, ms) {
  let t = { promise: null, cancel: _ => void 0 }
  return [
   // ...,
    _ => t.cancel() // ✅ return cancellation mechanism
  ]
}
// revised usage
const [inc, cancel] = debounce(clickCounter, 1000) // ✅ two controls
myform.mybutton.addEventListener("click", inc)
myform.mycancel.addEventListener("click", cancel)

實現一個useDebounce React 鈎子輕而易舉 -

function useDebounce(task, ms) {
  const [f, cancel] = debounce(task, ms)
  useEffect(_ => cancel) // ✅ auto-cancel when component unmounts
  return [f, cancel]
}

前往原始問答以獲得完整的演示

簡單的去抖功能:-

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

您想要做的是:如果您嘗試一個接一個地調用一個函數,第一個應該被取消,新的應該等待給定的超時然后執行。 所以實際上你需要某種方法來取消第一個函數的超時? 但是怎么做? 可以調用該函數,並傳遞返回的 timeout-id,然后將該 ID 傳遞給任何新函數。 但是上面的解決方案更加優雅。

它的作用是有效地使timeout變量在返回函數的范圍內可用。 因此,當觸發“調整大小”事件時,它不會再次調用debounce() ,因此timeout內容不會更改(!)並且仍然可用於“下一個函數調用”。

這里的關鍵基本上是每次我們有調整大小事件時都會調用內部函數。 如果我們想象所有的調整大小事件都在一個數組中,也許會更清楚:

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了嗎? 在我看來,沒有理由將其重命名為args this contentarguments

這是一個變體,它總是在第一次調用時觸發去抖動函數,並使用更具描述性的變量命名:

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

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/

下面是debounce函數作用的總結,用幾行演示解釋。

debounce函數是一個函數,它將:

  • 在第一次執行時,使用setTimeout函數安排包裝函數在一段時間后執行
  • (如果在此時間間隔內再次執行):
    • 刪除之前的計划(使用clearTimeOut功能)
    • 重新安排一個新的(使用setTimeout功能)

並且循環一直持續到時間間隔過去並且包裝的函數執行為止。

改編自所有評論和本文

 function debounce(callBack, interval, leadingExecution) { // the schedule identifier, if it's not null/undefined, a callBack function was scheduled let timerId; return function () { // Does the previous run has schedule a run let wasFunctionScheduled = (typeof timerId === 'number'); // Delete the previous run (if timerId is null, it does nothing) clearTimeout(timerId); // Capture the environment (this and argument) and wraps the callback function let funcToDebounceThis = this, funcToDebounceArgs = arguments; let funcToSchedule = function () { // Reset/delete the schedule clearTimeout(timerId); timerId = null; // trailing execution happens at the end of the interval if (!leadingExecution) { // Call the original function with apply callBack.apply(funcToDebounceThis, funcToDebounceArgs); } } // Schedule a new execution at each execution timerId = setTimeout(funcToSchedule, interval); // Leading execution if (!wasFunctionScheduled && leadingExecution) callBack.apply(funcToDebounceThis, funcToDebounceArgs); } } function onMouseMove(e) { console.log(new Date().toLocaleString() + ": Position: x: " + ex + ", y:" + ey); } let debouncedMouseMove = debounce(onMouseMove, 500); document.addEventListener('mousemove', debouncedMouseMove);

如果你使用的是react.js

  function debounce(func, delay = 600) {
    return (args) => {
      clearTimeout(timeout.current);
      timeout.current = setTimeout(() => {
        func(args);
      }, delay);
    };
  }

  const triggerSearch = debounce(handleSearch);
  // Event which triggers search.
  onSearch={(searchedValue) => {
        setSearchedText(searchedValue);// state update
        triggerSearch(searchedValue);
      }}

由於搜索事件中的 state 更新會在每種字母類型上觸發,因此需要重新渲染,並且所有具有debounce func的代碼也將重新啟動。

由於反應的這種行為,從來沒有主動超時。

 function debounce(func, delay = 600) {
    return (args) => {
      clearTimeout(timeout.current);
      timeout.current = setTimeout(() => {
        func(args);
      }, delay);
    };
  }
 const triggerSearch = debounce(handleSearch);

為了解決這個問題,我使用了一個名為timeoutref

 const timeout = useRef();

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM