在useEffect中遇到去抖动问题

55

我有一个包含用户名输入框的表单,我正在尝试在防抖函数中验证该用户名是否已被使用。 问题是我的防抖似乎没有起作用,因为当我键入“user”时,我的控制台看起来像

u
us
use
user

这是我的防抖函数

export function debounce(func, wait, immediate) {
    var timeout;

    return () => {
        var context = this, args = arguments;

        var later = () => {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };

        var callNow = immediate && !timeout;

        clearTimeout(timeout);

        timeout = setTimeout(later, wait);

        if (callNow) func.apply(context, args);
    };
};

这是我在React组件中调用它的方式

import React, { useEffect } from 'react' 

// verify username
useEffect(() => {
    if(state.username !== "") {
        verify();
    }
}, [state.username])

const verify = debounce(() => {
    console.log(state.username)
}, 1000);
似乎防抖功能是正确的?我在react中调用它有问题吗?

防抖函数似乎是正确的?我在react中的调用方式有问题吗?

5个回答

101
每次组件重新渲染时,都会创建一个新的防抖函数verify,这意味着在useEffect中实际上正在调用不同的函数,这破坏了防抖的目的。 这就像你正在做这样的事情:
const debounced1 = debounce(() => { console.log(state.username) }, 1000);
debounced1();

const debounced2 = debounce(() => { console.log(state.username) }, 1000);
debounced2();

const debounced3 = debounce(() => { console.log(state.username) }, 1000);
debounced3();

与您真正想要的相反:

const debounced = debounce(() => { console.log(state.username) }, 1000);
debounced();
debounced();
debounced();

解决这个问题的一种方法是使用useCallback,它将始终返回相同的回调函数(当您将空数组作为第二个参数传入时)。另外,我建议将username传递给该函数,而不是在内部访问状态(否则,您将访问过时的状态):

import { useCallback } from "react";
const App => () {
  const [username, setUsername] = useState("");

  useEffect(() => {
    if (username !== "") {
      verify(username);
    }
  }, [username]);

  const verify = useCallback(
    debounce(name => {
      console.log(name);
    }, 200),
    []
  );

  return <input onChange={e => setUsername(e.target.value)} />;
}

同时,您需要稍微更新防抖函数,因为它没有正确地将参数传递给被防抖函数。

function debounce(func, wait, immediate) {
  var timeout;

  return (...args) => { <--- needs to use this `args` instead of the ones belonging to the enclosing scope
    var context = this;
...

演示

注意:您将看到一个 ESLint 警告,指出 useCallback 需要一个内联函数,您可以通过使用 useMemo 来解决这个问题,并知道 useCallback(fn, deps) 等效于 useMemo(() => fn, deps)

const verify = useMemo(
  () => debounce(name => {
    console.log(name);
  }, 200),
  []
);

19
export function useLazyEffect(effect: EffectCallback, deps: DependencyList = [], wait = 300) {
  const cleanUp = useRef<void | (() => void)>();
  const effectRef = useRef<EffectCallback>();
  const updatedEffect = useCallback(effect, deps);
  effectRef.current = updatedEffect;
  const lazyEffect = useCallback(
    _.debounce(() => {
      cleanUp.current = effectRef.current?.();
    }, wait),
    [],
  );
  useEffect(lazyEffect, deps);
  useEffect(() => {
    return () => {
      cleanUp.current instanceof Function ? cleanUp.current() : undefined;
    };
  }, []);
}

2
不错的通用版本!这个答案值得更多的赞。也许可以添加一个使用示例(即使它可能看起来很明显)。并且可以添加一些解释性注释。 - Christiaan Westerbeek
2
这是一个很好的函数,已经包装在一个漂亮的Gist中,并且已经导入并准备好使用 :) https://gist.github.com/felipecsl/afb987f8b6059814cff0a2ca6020e108 - Felipe Lima
1
一些解释会让这个答案更好,而决定它是否“更好”则取决于读者。 - isherwood

17

我建议进行一些更改。

1)每次状态改变时,都会触发一次渲染。每个渲染都有自己的属性和效果。因此,您的useEffect在更新用户名时每次生成一个新的抖动函数。这是使用useCallback钩子来保持函数实例在渲染之间保持相同的一个很好的例子,或者可能是useRef - 我自己坚持使用useCallback。

2)我会将单独的处理程序分离出来,而不是使用useEffect来触发抖动 - 随着组件的增长,您最终会得到一个很长的依赖项列表,这并不是最好的位置。

3)您的抖动函数不处理参数。(我用lodash.debouce替换了它,但您可以调试自己的实现)

4)我认为您仍然希望在按键时更新状态,但只是每隔一定时间运行抖动函数

示例:

import React, { useState, useCallback } from "react";
import "./styles.css";
import debounce from "lodash.debounce";

export default function App() {
  const [username, setUsername] = useState('');

  const verify = useCallback(
    debounce(username => {
      console.log(`processing ${username}`);
    }, 1000),
    []
  );

  const handleUsernameChange = event => {
    setUsername(event.target.value);
    verify(event.target.value);
  };

  return (
    <div className="App">
      <h1>Debounce</h1>
      <input type="text" value={username} onChange={handleUsernameChange} />
    </div>
  );
}

演示

我强烈推荐阅读这篇关于 useEffect 和 Hooks 的好 文章


你认为把[state, setState] = { step: 1, name: "", email: "", username: "", password: "", confirm: "", error: "", verified: "", valid: false }拆分成单独的useState是一个好建议吗?我理解为什么在处理用户名和去抖动时这样做。 - Ryne
是的,我会将状态和相关处理程序分开,这样你就可以拥有 [username, setUsername] handleUsernameChange() [password, setPassword] handlePasswordChange() 等。 - Samuel Goldenbaum

0
一个使用 useEffectuseState 钩子实现的简单的 debounce 功能。
import {useState, useEffect} from 'react';

export default function DebounceInput(props) {
    const [timeoutId, setTimeoutId] = useState();

    useEffect(() => {
        return () => {
            clearTimeout(timeoutId);
        };
    }, [timeoutId]);

    function inputHandler(...args) {
        setTimeoutId(
            setTimeout(() => {
                getInputText(...args);
            }, 250)
        );
    }

    function getInputText(e) {
        console.log(e.target.value || "Hello World!!!");
    }

    return (
        <>
            <input type="text" onKeyDown={inputHandler} />
        </>
    );
}

希望这个能够正常工作。下面是我附上的原生JavaScript代码,用于实现debounce函数。

function debounce(cb, delay) {
    let timerId;
    return (...args) => {
        if (timerId) clearTimeout(timerId);
        timerId = setTimeout(() => {
            cb(...args);
        }, delay);
    };
}

function getInputText(e){
    console.log(e.target.value);
}

const input = document.querySelector('input');
input.addEventListener('keydown',debounce(getInputText,500));

-1

这是一个使用纯JavaScript编写的自定义钩子,可以实现防抖的useEffect

export const useDebounce = (func, timeout=100) => {
    let timer;
    let deferred = () => {
        clearTimeout(timer); 
        timer = setTimeout(func, timeout);
    };
    const ref = useRef(deferred);
    return ref.current;
};

export const useDebouncedEffect = (func, deps=[], timeout=100) => {
    useEffect(useDebounce(func, timeout), deps);
}

对于你的例子,你可以这样使用:

useDebouncedEffect(() => {
    if(state.username !== "") {
        console.log(state.username);
    }
}, [state.username])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接