去抖而不取消
VLAZ 链接了Can someone explain the "debounce" function in Javascript?,但您似乎很失望并正在寻找具有取消机制的东西。我对该问题提供的答案实现了一个香草debounce -
|
|
| ✅ |
At most one promise pending at any given time (per debounced task) |
| ✅ |
Stop memory leaks by properly cancelling pending promises |
| ✅ |
Resolve only the latest promise |
| ❌ |
Expose cancellation mechanism |
我们用两个参数编写了debounce,task 去抖动,以及延迟的毫秒数,ms。我们为其本地状态引入了单个本地绑定,t -
// original implementation
function debounce(task, ms) {
let t = { promise: null, cancel: _ => void 0 }
return async (...args) => { // ⚠️ does not return cancel mechanism
try {
t.cancel()
t = deferred(ms)
await t.promise
await task(...args)
}
catch (_) { /* prevent memory leak */ }
}
}
// original usage
// ⚠️ how to cancel?
myform.mybutton.addEventListener("click", debounce(clickCounter, 1000))
现在有外部取消
原始代码大小平易近人,不到 10 行,旨在供您修改以满足您的特定需求。我们可以通过简单地将取消机制包含在其他返回值中来公开取消机制 -
// revised implementation
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 */ }
},
_ => 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)
延期
debounce 依赖于一个可重用的deferred 函数,它创建了一个在ms 毫秒内解析的新promise。在链接的问答中阅读更多相关信息 -
function deferred(ms) {
let cancel, promise = new Promise((resolve, reject) => {
cancel = reject
setTimeout(resolve, ms)
})
return { promise, cancel }
}
取消演示
运行下面的sn-p。 Click 去抖动一 (1) 秒。去抖定时器到期后,计数器递增。但是,如果您在inc 去抖时单击取消,则挂起的函数将被取消,并且计数器不会增加。
// debounce, compressed for demo
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 */ } }, _ => t.cancel() ]
}
// deferred, compressed for demo
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
[inc, cancel] = debounce(clickCounter, 1000)
myform.myclicker.addEventListener("click", inc)
myform.mycancel.addEventListener("click", cancel)
<form id="myform">
<input name="myclicker" type="button" value="click" />
<input name="mycancel" type="button" value="cancel" />
<output name="mycounter">0</output>
</form>
类型
deferred 和 debounce 的一些合理注释,供考虑类型的人使用。
// cancel : () -> void
//
// waiting : {
// promise: void promise,
// cancel: cancel
// }
//
// deferred : int -> waiting
function deferred(ms) {
let cancel, promise = new Promise((resolve, reject) => {
cancel = reject
setTimeout(resolve, ms)
})
return { promise, cancel }
}
// 'a task : (...any -> 'a)
//
// debounce : ('a task, int) -> ('a task, cancel)
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 */ }
},
_ => t.cancel()
]
}
反应钩子
用debounce 实现useDebounce 非常简单。卸载组件时记得cancel,以防止任何悬空去抖动操作-
function useDebounce(task, ms) {
const [f, cancel] = debounce(task, ms)
useEffect(_ => cancel) // ✅ auto-cancel when component unmounts
return [f, cancel]
}
将useDebounce 添加到您的组件与我们在上面使用香草debounce 的方式相同。如果对状态突变进行去抖动,请确保使用functional updates,因为 setter 将被异步调用 -
function App() {
const [count, setCount] = React.useState(0)
const [inc, cancel] = useDebounce(
_ => setCount(x => x + 1), // ✅ functional update
1000
)
return <div>
<button onClick={inc}>click</button>
<button onClick={cancel}>cancel</button>
<span>{count}</span>
</div>
}
react debounce 演示
这个demo和上面唯一的一样,只用了React和我们的useDebounce钩子-
// debounce, compressed for demo
function debounce(task, ms) {
let t = { promise: null, cancel: _ => void 0 }
return [ (...args) => { t.cancel(); t = deferred(ms); t.promise.then(_ => task(...args)).catch(_ => {}) }, _ => t.cancel() ]
}
// deferred, compressed for demo
function deferred(ms) {
let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel }
}
function useDebounce(task, ms) {
const [f, cancel] = debounce(task, ms)
React.useEffect(_ => cancel)
return [f, cancel]
}
function App() {
const [count, setCount] = React.useState(0)
const [inc, cancel] = useDebounce(
_ => setCount(x => x + 1),
1000
)
return <div>
<button onClick={inc}>click</button>
<button onClick={cancel}>cancel</button>
<span>{count}</span>
</div>
}
ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
多次去抖动
让我们仔细检查一切是否正确,并显示在同一页面上使用了多个 debounce。我们将通过添加更多调用相同去抖动函数的 Click 按钮来扩展计数器示例。我们将在同一页面上放置多个计数器,以表明多个去抖动器保持单独控制并且不会中断其他去抖动器。这是应用程序的预览 -
运行演示并验证这些行为 -
|
|
| ✅ |
3 Counters, each with their own counter state |
| ✅ |
Each counter has 3 debounced Click buttons and a single Cancel button |
| ✅ |
Each Click can be used to increment the counter's value |
| ✅ |
Each Click will interrupt any debounced increment from other Click belonging to that counter |
| ✅ |
The Cancel button will cancel debounced increments from any Click belonging to that counter |
| ✅ |
Cancel will not cancel debounced increments belonging to other counters |
function debounce(task, ms) { let t = { promise: null, cancel: _ => void 0 }; return [ (...args) => { t.cancel(); t = deferred(ms); t.promise.then(_ => task(...args)).catch(_ => {}) }, _ => t.cancel() ] }
function deferred(ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }
function useDebounce(task, ms) {const [f, cancel] = debounce(task, ms); React.useEffect(_ => cancel); return [f, cancel] }
function useCounter() {
const [count, setCount] = React.useState(0)
const [inc, cancel] = useDebounce(
_ => setCount(x => x + 1),
1000
)
return [count, <div className="counter">
<button onClick={inc}>click</button>
<button onClick={inc}>click</button>
<button onClick={inc}>click</button>
<button onClick={cancel}>cancel</button>
<span>{count}</span>
</div>]
}
function App() {
const [a, counterA] = useCounter()
const [b, counterB] = useCounter()
const [c, counterC] = useCounter()
return <div>
{counterA}
{counterB}
{counterC}
<pre>Total: {a+b+c}</pre>
</div>
}
ReactDOM.render(<App/>, document.querySelector("#app"))
.counter { padding: 0.5rem; margin-top: 0.5rem; background-color: #ccf; }
pre { padding: 0.5rem; background-color: #ffc; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>