React 中的去抖动和超时

Posted

技术标签:

【中文标题】React 中的去抖动和超时【英文标题】:Debouncing and Timeout in React 【发布时间】:2022-01-13 01:47:50 【问题描述】:

我在这里有一个输入字段,在每种类型上,它都会调度一个 redux 操作。 我放了一个 useDebounce 以使它不会很重。问题是它说Hooks can only be called inside of the body of a function component. 正确的做法是什么?

使用超时

import  useCallback, useEffect, useRef  from "react";

export default function useTimeout(callback, delay) 
  const callbackRef = useRef(callback);
  const timeoutRef = useRef();

  useEffect(() => 
    callbackRef.current = callback;
  , [callback]);

  const set = useCallback(() => 
    timeoutRef.current = setTimeout(() => callbackRef.current(), delay);
  , [delay]);

  const clear = useCallback(() => 
    timeoutRef.current && clearTimeout(timeoutRef.current);
  , []);

  useEffect(() => 
    set();
    return clear;
  , [delay, set, clear]);

  const reset = useCallback(() => 
    clear();
    set();
  , [clear, set]);

  return  reset, clear ;

使用去抖动

import  useEffect  from "react";
import useTimeout from "./useTimeout";

export default function useDebounce(callback, delay, dependencies) 
  const  reset, clear  = useTimeout(callback, delay);
  useEffect(reset, [...dependencies, reset]);
  useEffect(clear, []);

表单组件

import React from "react";
import TextField from "@mui/material/TextField";
import useDebounce from "../hooks/useDebounce";

export default function ProductInputs(props) 
  const  handleChangeProductName = () =>   = props;

  return (
    <TextField
      fullWidth
      label="Name"
      variant="outlined"
      size="small"
      name="productName"
      value=formik.values.productName
      helperText=formik.touched.productName ? formik.errors.productName : ""
      error=formik.touched.productName && Boolean(formik.errors.productName)
      onChange=(e) => 
        formik.setFieldValue("productName", e.target.value);
        useDebounce(() => handleChangeProductName(e.target.value), 1000, [
          e.target.value,
        ]);
      
    />
  );

【问题讨论】:

是的,这绝对是放置钩子的错误位置。钩子应该放置在渲染元素之外。将其移动到TextField的父组件主体内 你的钩子是从你的组件内部的一个函数中调用的,这违反了hooks的规则你应该在顶层使用钩子。 @smac89。那么你将如何移动它并从中调用呢? 您自己定义了useDebounce 吗?你打算如何使用它? @Bergi。更新了我的问题。我想使用handleChangeProductName 向redux 发送一个动作,而不是在每个输入上,因为我有很多文本字段,所以它会很重 【参考方案1】:

我认为 React 钩子不适合用于油门或去抖动功能。根据我对您的问题的理解,您实际上希望消除 handleChangeProductName 函数的抖动。

这是一个简单的高阶函数,您可以使用它来装饰回调函数以消除抖动。如果在超时到期之前再次调用返回的函数,则超时将被清除并重新实例化。只有当超时过期时,装饰函数才会被调用并传递参数。

const debounce = (fn, delay) => 
  let timerId;
  return (...args) => 
    clearTimeout(timerId);
    timerId = setTimeout(() => fn(...args), delay);
  
;

示例用法:

export default function ProductInputs( handleChangeProductName ) 
  const debouncedHandler = useCallback(debounce(handleChangeProductName, 200), []);

  return (
    <TextField
      fullWidth
      label="Name"
      variant="outlined"
      size="small"
      name="productName"
      value=formik.values.productName
      helperText=formik.touched.productName ? formik.errors.productName : ""
      error=formik.touched.productName && Boolean(formik.errors.productName)
      onChange=(e) => 
        formik.setFieldValue("productName", e.target.value);
        debouncedHandler(e.target.value);
      
    />
  );

如果可能,将 handleChangeProductName 回调作为 prop 传递的父组件应该可以处理创建去抖动、记忆化的处理程序,但上述方法也应该可以工作。

【讨论】:

@Joseph 如果用户正在积极输入,您不妨等待他们完成,这样您就不会对已更改的输入进行浪费的中间异步调用。 你怎么能这样做呢?你也可以用它来编辑你的答案吗?谢谢德鲁 @Joseph 您将去抖动延迟调整为更长的时间,并在用户完成输入时“猜测”。或者换句话说,延迟是用户停止输入后调用函数所需的时间。【参考方案2】:

看看你对useDebounce 的实现,它作为一个钩子看起来不是很有用。它似乎已经接管了调用你的函数的工作,并且不返回任何东西,但它的大部分实现都是在 useTimeout 中完成的,它也没有做太多......

在我看来,useDebounce 应该返回 callback 的“去抖动”版本

这是我对useDebounce的看法:

export default function useDebounce(callback, delay) 
  const [debounceReady, setDebounceReady] = useState(true);

  const debouncedCallback = useCallback((...args) => 
    if (debounceReady) 
      callback(...args);
      setDebounceReady(false);
    
  , [debounceReady, callback]);

  useEffect(() => 
    if (debounceReady) 
      return undefined;
    
    const interval = setTimeout(() => setDebounceReady(true), delay);
    return () => clearTimeout(interval);    
  , [debounceReady, delay]);

  return debouncedCallback;


用法如下所示:

import React from "react";
import TextField from "@mui/material/TextField";
import useDebounce from "../hooks/useDebounce";

export default function ProductInputs(props) 
  const handleChangeProductName = useCallback((value) => 
    if (props.handleChangeProductName) 
      props.handleChangeProductName(value);
     else 
      // do something else...
    ;
  , [props.handleChangeProductName]);

  const debouncedHandleChangeProductName = useDebounce(handleChangeProductName, 1000);

  return (
    <TextField
      fullWidth
      label="Name"
      variant="outlined"
      size="small"
      name="productName"
      value=formik.values.productName
      helperText=formik.touched.productName ? formik.errors.productName : ""
      error=formik.touched.productName && Boolean(formik.errors.productName)
      onChange=(e) => 
        formik.setFieldValue("productName", e.target.value);
        debouncedHandleChangeProductName(e.target.value);
      
    />
  );

【讨论】:

谢谢。但是如何将它集成到 TextField 中? @约瑟夫。见编辑 似乎只是传递了props.handleChangeProductName上的第一个字母 @Joseph 尝试减少去抖动时间。坦率地说,1 秒的等待时间有点太长了。尝试像 150 这样更小的值。要考虑的其他一点是 debounce 仅在超时后采用最新值,因此您可能需要更新 useDebounce 挂钩以跟踪传递给 debounce 的最后一个参数并在超时后调用该函数结束了 你能用那个编辑你的答案吗?【参考方案3】:

去抖onChange 本身有一些注意事项。比如说,它必须是不受控制的组件,因为在受控组件上去抖动 onChange 会导致打字时出现烦人的延迟。

另一个陷阱,我们可能需要立即做某事,延迟后再做其他事情。比如说,在任何更改后立即显示加载指示器而不是(过时的)搜索结果,但只有在用户停止输入后才发送实际请求。

考虑到这一切,我建议通过useEffect 去抖动同步,而不是去抖回调:

const [text, setText] = useState('');
const isValueSettled = useIsSettled(text);

useEffect(() => 
  if (isValueSettled) 
    props.onChange(text);
  
, [text, isValueSettled]);

...
  <input value=value onChange=( target:  value  ) => setText(value)

useIsSetlled 本身会去抖动:

function useIsSettled(value, delay = 500) 
  const [isSettled, setIsSettled] = useState(true);
  const isFirstRun = useRef(true);
  const prevValueRef = useRef(value);

  useEffect(() => 
    if (isFirstRun.current) 
      isFirstRun.current = false;
      return;
    
    setIsSettled(false);
    prevValueRef.current = value;
    const timerId = setTimeout(() => 
      setIsSettled(true);
    , delay);
    return () =>  clearTimeout(timerId); 
  , [delay, value]);
  if (isFirstRun.current) 
    return true;
  
  return isSettled && prevValueRef.current === value;

isFirstRun 显然可以避免我们在初始渲染后收到“哦,不,用户更改了某些内容”(当 valueundefined 更改为初始值时)。

并且prevValueRef.current === value 不是必需的部分,但确保我们将在同一渲染运行中得到useIsSettled 返回false,而不是在下一次,只有在执行useEffect 之后。

【讨论】:

以上是关于React 中的去抖动和超时的主要内容,如果未能解决你的问题,请参考以下文章

golang 陈有超时的去

jQuery中的去抖动功能

React Native - 无法清除超时

Qt 中的去抖动事件过滤器

React hooks - 清除超时和间隔的正确方法

带有 React-Native 的 android 模拟器上的位置请求超时