【问题标题】:How do I implement debounce in ramda如何在 ramda 中实现去抖动
【发布时间】:2022-06-16 02:06:12
【问题描述】:

我很确定答案是不可能的,但我想知道是否可以使用 Ramda 实现 lodash.debounce,这样我就可以摆脱我的应用程序中的 lodash 依赖,因为它只是那个。

这是我正在使用的代码

import debounce from "lodash.debounce";
import { Dispatch, useCallback, useState } from "react";


/**
 * This is a variant of set state that debounces rapid changes to a state.
 * This perform a shallow state check, use {@link useDebouncedDeepState}
 * for a deep comparison.  Internally this uses
 * [lodash debounce](https://lodash.com/docs/#debounce) to perform
 * the debounce operation.
 * @param initialValue initial value
 * @param wait debounce wait
 * @param debounceSettings debounce settings.
 * @returns state and setter
 *
 */
export function useDebouncedState<S>(
  initialValue: S,
  wait: number,
  debounceSettings?: Parameters<typeof debounce>[2]
): [S, Dispatch<S>] {
  const [state, setState] = useState<S>(initialValue);
  const debouncedSetState = useCallback(
    debounce(setState, wait, debounceSettings),
    [wait, debounceSettings]
  );
  useEffect(()=> {
    return () => debouncedSetState.cancel();
  }, []);
  return [state, debouncedSetState];
}

【问题讨论】:

  • 为什么不直接使用纯 JavaScript? Can someone explain the "debounce" function in Javascript Ramda 在实现这个非常标准且广为人知的功能方面真正有用吗?
  • 提供的示例没有 lodash.debounce 的细微差别。其中之一是取消语义,我刚刚注意到我的代码没有使用它,它会解释我在卸载组件时发生状态变化的错误。
  • 我仍然看不到 Ramda 与实现去抖动有什么关系。如果您想取消,那仍然与另一个图书馆无关。如您所见,debounding 只有两个真正相关的组件 - 延迟(您不需要库)和通过保留 this 调用函数(这也是您不需要库的)。取消机制是延迟中的额外if,以决定是否触发延迟功能。您想在哪些任务中使用 Ramda?
  • Ramda 不包含像 debounce 这样的任何东西,因为它似乎与 Ramda 的哲学不太相符。 (免责声明:我是 Ramda 的创始人。)但正如 VLAZ 所说,创建自己的并不难。我怀疑 Ramda 函数会对此有多大帮助,但你永远不知道!

标签: javascript ramda.js debouncing


【解决方案1】:

去抖而不取消

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>

【讨论】:

    猜你喜欢
    • 2013-11-17
    • 2017-07-01
    • 1970-01-01
    • 2019-12-31
    • 1970-01-01
    • 1970-01-01
    • 2021-09-08
    • 2017-01-30
    • 2021-09-20
    相关资源
    最近更新 更多