[英]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
的一般思路如下:
第一點就是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
content
和arguments
。
這是一個變體,它總是在第一次調用時觸發去抖動函數,並使用更具描述性的變量命名:
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
函數是一個函數,它將:
clearTimeOut
功能)並且循環一直持續到時間間隔過去並且包裝的函數執行為止。
改編自所有評論和本文
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);
為了解決這個問題,我使用了一個名為timeout
的ref
。
const timeout = useRef();
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.