JavaScript 中的“ deounce”函数是什么?

我对在 JavaScript 的反弹函数上的 JavaScript 中的“去弹性”函数很感兴趣。

不幸的是代码解释得不够清楚,我无法理解。它是如何工作的(我在下面留下了我的评论) ?简而言之,我真的不明白这是怎么回事。

   // 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放在了错误的位置。

113432 次浏览

调用时不执行已退回的函数。它们在执行之前等待可配置持续时间内的调用暂停; 每次新的调用都会重新启动计时器。

节流函数执行,然后等待一段可配置的持续时间,然后才有资格再次激发。

对于按键事件来说,退回非常有用; 当用户开始键入然后暂停时,您可以将所有按键作为一个事件提交,从而减少了处理调用。

节流对于实时端点非常有用,您只希望允许用户在设定的时间段内调用一次实时端点。

也可以查看 下划线.js的实现。

这里需要注意的重要事情是,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似乎有一个稍微不同的检查,在那里它评估 immediate && !timeout 之前调用 setTimeout。(算法也有点不同,例如它不使用 clearTimeout。)这就是为什么你应该总是尝试使用你的库的最新版本。:-)

问题中的代码与链接中的代码略有不同。在该链接中,检查 (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(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);

您需要执行以下操作: 如果您尝试一个接一个地调用一个函数,那么第一个函数应该是 取消了,而新的函数应该等待给定的超时时间,然后执行。因此,实际上您需要某种方法来取消第一个函数的超时?但是怎么做呢? 可以调用该函数,并传递返回的 timeout-ID,然后将该 ID 传递给任何新函数。但是上面的解决方案要优雅得多。

它有效地使 timeout变量在返回函数的范围内可用。因此,当触发“ resize”事件时,它不会再次调用 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可用于下一个迭代了吗? 我认为没有理由把 this改名为 content,把 arguments改名为 args

我写了一篇题为 在 JavaScript 中消除弹出的文章,在那里我解释了确切的 退出函数是如何工作的,并包括一个演示。

当我第一次遇到这样一个函数时,我也没有完全理解它是如何工作的。尽管它们的大小相对较小,但它们实际上采用了一些相当高级的 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);
}
};
};

这是一个变体,它总是在第一次调用反向函数时触发该函数,并使用更具描述性的命名变量:

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 中有一个简单的 deounce 方法:

基本 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>

JavaScript 文件

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

司机代码

debouncebtn.addEventListener('click', debounce(function() {
document.getElementById('message').innerHTML += '<br/> The button only triggers every 3 seconds how much every you fire an event';
console.log('The button only triggers every 3 seconds how much every you fire an event');
}, 3000))

运行时示例 JSFiddle: https://jsfiddle.net/arbaazshaikh919/d7543wqe/10/

一个简单的反弹函数:

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

我们现在都在用承诺

我见过的许多实现都将问题过于复杂化,或者存在其他卫生问题。现在是2021年,我们已经使用承诺很长时间了,而且有很好的理由。承诺清理异步程序并减少发生错误的机会。在这篇文章中,我们将写我们自己的 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本身,以及 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>

“自动完成”实时查询示例

在第二个示例中,我们有一个带有文本输入的表单

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

多次反弹,反作用钩使用反弹

另一个问答环节中,有人询问是否可以使用公开退出取消机制并创建一个 useDebounce反应钩。使用上面的 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反应钩是一件轻而易举的事

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

前往 原创问答为一个完整的演示

下面是 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: " + e.x + ", y:" + e.y);
}


let debouncedMouseMove = debounce(onMouseMove, 500);


document.addEventListener('mousemove', debouncedMouseMove);