【问题标题】:Can't get `lodash.debounce()` to work properly? Executed multiple times... (react, lodash, hooks)无法让 `lodash.debounce()` 正常工作?多次执行......(反应,lodash,钩子)
【发布时间】:2020-03-29 16:43:52
【问题描述】:

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

然而,在 debounce 的延迟之后,所有的事件都被触发了。好像我在每个范围输入事件上连续调用了setTimeout 函数,但不是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))
          }}
        />
  }

【问题讨论】:

    标签: reactjs lodash react-hooks


    【解决方案1】:

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

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

    const [debouncedChange] = React.useState(_.debounce(
        (field, payload) => handleChangeAt(field, payload),
        500,
    ))
    

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

    【讨论】:

    • 嗯,但是,在我的代码中,const debounceChange = _.debounce(...) 已经是一个变量,因为这是一个功能组件,并且它只被创建一次,不是吗?
    • 您对问题的诊断是正确的,但使用钩子的实际解决方案更为复杂。
    • @Retsam 如果您知道如何使用钩子实现去抖动,能否分享您的解决方案?
    • 不,功能组件的主体在每次组件渲染时运行。代码的某些部分只运行一次(即评估状态变量的初始值,或 useEfffect 的特殊情况),但这只是 React 幕后的魔法。其余代码照常运行@Ege
    【解决方案2】:

    问题

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

    您需要跨渲染重用相同的功能。有一种使用钩子的简单方法......还有一种更好的方法:

    简单的(有缺陷的)解决方案 - useCallback

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

    const debouncedCallback = useCallback(_.debounce(
        (field, payload) => handleChangeAt(field, payload),
        500,
    ), [handleChangeAt])
    

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

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

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

    (如果你不熟悉这种记忆,我强烈推荐 Dan Abramov 的 Complete Guide to useEffect,尽管我们实际上并没有在这里使用 useEffect

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

    更好的解决方案 - useRef

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

    但是为了避免陈旧数据而重新创建将导致函数被重新创建,这将导致去抖动重置:所以之前解决方案的结果是通常去抖动,但如果 props 或 state 发生了变化,可能不会。

    更好的解决方案要求我们实际上只创建一次记忆函数,但这样做的方式是避免过时的数据。我们可以通过使用 ref 来做到这一点:

    const debouncedFunctionRef = useRef()
    debouncedFunctionRef.current = (field, payload) => handleChangeAt(field, payload);
    
    const debouncedChange = useCallback(_.debounce(
        (...args) => debouncedFunctionRef.current(...args),
        500,
    ), []);
    

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

    由于回调唯一依赖的是一个 ref(它是一个常量、可变对象),所以 useCallback 可以将 [] 作为它的依赖项,因此我们每次只对函数进行一次反跳组件,正如预期的那样。

    作为自定义钩子

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

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

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

    【讨论】:

    • 非常感谢您的长篇解释 Restam,我花了一段时间才理解您的解释。但是,这无济于事-我尝试了您的结构并确认它对我不起作用。您的代码 sn-p 在这里完美运行,因为您在我的问题中遗漏了 2 件重要的事情: 1. 我正在尝试使用 debounce 更新 Parent 的状态,但我的 inputvalue 属性从 @ 读取987654353@ 的状态(通过钩子与父状态平行更新)。 2. 我的输入是range 输入,不是文本或按钮输入。
    • 对于您的代码 sn-p,尝试使其如下所示:将按钮替换为范围输入,将 UI 输入值设置为 Reporter 的状态并从那里读取并显示在屏幕上,但在同时,在父组件的状态“带去抖动”上设置相同的值,并将其显示在屏幕上。那是我的问题。
    • 我尝试了这里提出的前两种解决方案,但它们都不起作用,而且解释有点难以理解。最后useMemo 是另一个答案的方法。
    【解决方案3】:

    我使用了useCallback_.debounce,但是遇到了一个 eslint 错误,'React Hook useCallback 收到了一个依赖关系未知的函数。而是传递一个内联函数。'

    最后,我找到了this issue,并使用了useMemo

    之前:

     const debouncedMethod = useCallback(
        debounce((arg) => {
          someMethod(arg);
        }, 500),
        [someMethod],
      );
    

    之后:

      const debouncedMethod = useMemo(
        () =>
          debounce((arg) => {
            someMethod(arg);
          }, 500),
        [someMethod],
      );
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-12-17
      • 1970-01-01
      • 1970-01-01
      • 2021-06-20
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多