使用 useEffect 和 setInterval 一个接一个地打印一个字母

Posted

技术标签:

【中文标题】使用 useEffect 和 setInterval 一个接一个地打印一个字母【英文标题】:print one letter after the other with useEffect and setInterval 【发布时间】:2021-08-12 05:18:49 【问题描述】:

我试图了解使用useEffectsetInterval 创建一个每隔一段时间(如打字机)一个接一个地打印一个字母的组件的最佳方法是什么

这种方式会导致间隔永远运行:
function Printer( str ) 
  const [val, setVal] = useState(0);

  useEffect(() => 
    const interval = setInterval(() => 
      setVal((preVal) => preVal + 1);
    , 200);
    return () => 
      clearInterval(interval);
    ;
  , [str]);

  return <div>str.slice(0, val)</div>;

虽然这个人会抱怨 val 是 useEffect 依赖数组的缺失依赖:
function Printer( str ) 
  const [val, setVal] = useState(0);

  useEffect(() => 
    const interval = setInterval(() => 
      if (val < str.length) 
        setVal((preVal) => preVal + 1);
       else 
        clearInterval(interval);
      
    , 200);
    return () => 
      clearInterval(interval);
    ;
  , [str]);

  return <div>str.slice(0, val)</div>;

如果将 val 添加到依赖项数组中 - 我们将设置多个冗余间隔(并删除它们,因此感觉有点糟糕):
function Printer( str ) 
  const [val, setVal] = useState(0);

  useEffect(() => 
    const interval = setInterval(() => 
      if (val < str.length) 
        setVal((preVal) => preVal + 1);
       else 
        clearInterval(interval);
      
    , 200);
    return () => 
      clearInterval(interval);
    ;
  , [str, val]);

  return <div>str.slice(0, val)</div>;

不确定我是否遗漏了有关 useEffect、依赖数组或这只是 linter 拖钓我的东西。

【问题讨论】:

你的最后一种方法看起来不错,只是尝试不依赖于你的 useEffect,所以它只被调用一次。 【参考方案1】:

你可以使用useRef()来存储结束条件(stop):

const  useState, useRef, useEffect  = React;

function Printer( str ) 
  const [val, setVal] = useState(0);
  const stop = useRef();
  
  useEffect(() => 
    stop.current = val === str.length;
  );

  useEffect(() => 
    const interval = setInterval(() => 
      if(stop.current) clearInterval(interval);
      else setVal((preVal) => preVal + 1);
    , 200);
    return () => 
      clearInterval(interval);
    ;
  , []);

  return <div>str.slice(0, val)</div>;


ReactDOM.render(
  <Printer str="cats" />,
  root
);
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

<div id="root"></div>

【讨论】:

【参考方案2】:

将字符串 prop 和状态索引都放入依赖数组中,为了使事情更容易,使用超时而不是间隔。还添加一个在字符串 prop 更改时运行的回调,以便您可以将状态索引重置为 0。

const Printer = ( str ) => 
  const [index, setIndex] = React.useState(0);
  React.useEffect(() => 
    if (index < str.length) 
      const timeoutId = setTimeout(() => 
        setIndex(index + 1);
        return () => clearTimeout(timeoutId);
      , 200);
      return () => clearTimeout(timeoutId);
    
  , [index, str]);
  React.useEffect(() => 
    setIndex(0);
  , [str]);
  return <div>str.slice(0, index)</div>;

const App = () => 
  const [str, setStr] = React.useState('');
  return (
    <div>
      <input value=str onChange=(e) => setStr(e.currentTarget.value) />
      <Printer str=str />
    </div>
  );
;
ReactDOM.render(<App/>, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>

【讨论】:

是的,我想这不是必需的。我正在考虑在道具更改时立即重置索引,而不是在很短的延迟之后。 我可以看到使用setTimeout 而不是setInterval 背后的原因,但没有使用setState(在我们的例子中为setIndex)并包括状态(在我们的例子中为@987654327 @) 在依赖数组中是一种不好的做法(通常)? @mikey.nagler 一般来说,它绝对没有错。由于您希望在先前的状态更改之后发生后续的状态更改,因此将 setter 放入其中非常有意义 - 这是您正在寻找的逻辑,并且没有关于它的代码气味。当然,如果你没有if (index &lt; str.length) ,它会永远持续下去——可能需要,也可能不想要。 @CertainPerformance 所以这没问题,只要我们受到代码本身(或它背后的逻辑)的保护? (1) 效果要再次运行时调用(这里不重要),(2) 组件卸载时调用(这个很重要,以避免无操作、React 警告和潜在的内存泄漏)

以上是关于使用 useEffect 和 setInterval 一个接一个地打印一个字母的主要内容,如果未能解决你的问题,请参考以下文章

如何循环使用 useEffect 和 setState

使用 useEffect 和 jest.useFakeTimers() 进行测试

useState和useEffect

使用 React Native 和 useEffect 获取

useEffect和useLayoutEffect

使用 useEffect 和 setInterval 一个接一个地打印一个字母