无法让 `lodash.debounce()` 正常工作吗?多次执行...(反应,lodash,钩子)

Ege*_*Ege 6 lodash reactjs react-hooks

我试图在范围输入移动时更新 React 中子组件的状态。而且,我想使用 Lodash 的 debounce 函数将更新函数触发到父组件的状态,这样我就不会在每次范围输入触发事件时都设置父组件的状态。

但是,在 debounce 延迟之后,所有事件都被触发了。好像我setTimeout在每个范围输入事件上连续调用函数,但不是debounce.

我在这里找不到我缺少的东西。如何在一系列范围输入事件后执行一次传递给“debounce”的函数?

我的简化代码如下所示:

import _ from 'lodash'
import React from 'react'

const Form: React.FC<Props> = props => {
    const [selectedStorageSize, setSelectedStorageSize] = React.useState(props.storageSize)

    const handleChangeAt = (field, payload) => {
      props.handleFormChangeAt(FormField.InstanceDefs, {
        ...form[FormField.InstanceDefs],
        [field]: payload,
      })
    }

    const debouncedChange = _.debounce(
     (field, payload) => handleChangeAt(field, payload),
     500,
   )

   return(
        <input
          required
          type="range"
          label="Storage Size/GB"
          min={50}
          max={500}
          value={props.selectedStorageSize}
          step={5}
          onChange={e => {
            setSelectedStorageSize(Number(e.target.value))
            debouncedChange(FormField.StorageSize, Number(e.target.value))
          }}
        />
  }
Run Code Online (Sandbox Code Playgroud)

Ret*_*sam 15

问题

_.debounce创建一个函数,该函数可以消除对同一函数实例的连续调用。但这会在每次渲染时创建一个新的,因此连续调用不会调用同一个实例。

您需要在渲染中重用相同的功能。使用钩子有一种直接的方法可以做到这一点……还有一种更好的方法:

直接(有缺陷)的解决方案 - useCallback

在渲染中保留内容的最直接方法是useMemo/ useCallback。(useCallback实际上只是useMemofor 函数的一个特例)

const debouncedCallback = useCallback(_.debounce(
    (field, payload) => handleChangeAt(field, payload),
    500,
), [handleChangeAt])
Run Code Online (Sandbox Code Playgroud)

我们遇到了一个问题handleChangeAt:它依赖于 props 并在每次渲染时创建一个不同的实例,以便捕获最新版本的 props。如果我们只创建debouncedCallback一次,我们将捕获 的第一个实例handleChangeAt,并捕获 的初始值props,稍后为我们提供陈旧数据。

我们通过添加[handleChangeAt], 来解决这个问题,以重新创建debouncedCallback每当handleChangeAt更改时。但是,正如所写的那样,每次渲染都会handleChangeAt更改。因此,仅此更改不会改变初始行为:我们仍然会重新创建每个渲染。所以你也需要记住:debouncedCallbackhandleChangeAt

const { handleFormChangeAt } = props;
const handleChangeAt = useCallback((field, payload) => {
    handleFormChangeAt(/*...*/)
}, [handleFormChangeAt]);
Run Code Online (Sandbox Code Playgroud)

(如果你不熟悉这种记忆方式,我强烈推荐 Dan Abramov 的完整指南useEffect,即使我们实际上并没有useEffect在这里使用)

这会将问题推到树上,您需要确保任何组件提供props.handleFormChangeAt的也能记住它。但是,否则这个解决方案在很大程度上有效......

更好的解决方案—— useRef

前一个解决方案的两个问题:如前所述,它将记忆问题推到了树上(特别是因为您依赖于作为 prop 传递的函数),但重点是这样我们就可以在任何时候重新创建函数需要,以避免过时的数据。

但是重新创建以避免陈旧数据将导致重新创建函数,这将导致去抖动重置:因此,先前解决方案的结果通常会去抖动,但如果道具或状态已更改,则可能不会.

更好的解决方案要求我们真正只创建一次记忆函数,但要以一种避免陈旧数据的方式进行。我们可以通过使用 ref 来做到这一点:

const debouncedFunctionRef = useRef()
debouncedFunctionRef.current = (field, payload) => handleChangeAt(field, payload);

const debouncedChange = useCallback(_.debounce(
    (...args) => debouncedFunctionRef.current(...args),
    500,
), []);
Run Code Online (Sandbox Code Playgroud)

这将要在 ref 中去抖动的函数的当前实例存储在 ref 中,并在每次渲染时更新它(防止过时数据)。但是,我们没有直接去抖动该函数,而是去抖动一个包装函数,该函数从 ref 读取当前版本并调用它。

由于回调所依赖的唯一对象是 ref(它是一个常量、可变对象),因此可以useCallback[]其作为其依赖项,因此我们只会按预期对每个组件执行一次去抖动函数。

作为自定义钩子

这种方法可以移动到它自己的自定义钩子中:

const useDebouncedCallback = (callback, delay) => {
    const callbackRef = useRef()
    callbackRef.current = callback;
    return useCallback(_.debounce(
        (...args) => callbackRef.current(...args),
        delay,
    ), []);
};
Run Code Online (Sandbox Code Playgroud)

const { useCallback, useState, useRef, useEffect } = React;

const useDebouncedCallback = (callback, delay, opts) => {
  const callbackRef = useRef()
  callbackRef.current = callback;
  return useCallback(_.debounce(
      (...args) => callbackRef.current(...args),
      delay,
      opts
  ), []);
};

function Reporter({count}) {
  const [msg, setMsg] = useState("Click to record the count")
  const onClick = useDebouncedCallback(() => {
    setMsg(`The count was ${count} when you clicked`);
  }, 2000, {leading: true, trailing: false})
  return <div>
    <div><button onClick={onClick}>Click</button></div>
    {msg}
    </div>
}

function Parent() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setInterval(() => setCount(x => x+1), 500)
  }, [])
  return (
    <div>
      <div>The count is {count}</div>
      <Reporter count={count} />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Parent />, rootElement);
Run Code Online (Sandbox Code Playgroud)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
<div id="root" />
Run Code Online (Sandbox Code Playgroud)


Max*_*Max 1

lodash.debounce创建一个新的函数调用,该函数的调用将被反跳。用例场景是创建一次,将结果存储在变量中并多次调用它,以便调用被去抖。每次渲染组件时,您都会创建一个新的去抖函数,因此每次新渲染都会获得新的去抖范围

const debouncedChange = _.debounce(...)您只想对每个组件实例调用一次。我对钩子不太熟悉,但我猜你可以做到这一点

const [debouncedChange] = React.useState(_.debounce(
    (field, payload) => handleChangeAt(field, payload),
    500,
))
Run Code Online (Sandbox Code Playgroud)

这将在渲染期间创建一次函数,并重用在连续渲染中创建的内容