我如何在 ramda 中实现去抖动

How do I implement debounce in ramda

我很确定答案是不可能的,但我想知道是否可以使用 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];
}

去抖而不取消

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

我们用两个参数编写了 debouncetask 去抖,以及延迟的毫秒数,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 毫秒内解析的新承诺。在链接的问答中阅读更多相关信息 -

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

取消演示

运行 下面的片段。 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>

类型

deferreddebounce 的一些合理注释,供思考类型的人使用。

// 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 添加到您的组件与我们在上面使用 vanilla 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>
}

反应去抖演示

这个demo和上面的一样,只用了React和我们的useDebounce hook -

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