使用 setTimeout 延迟在 useEffect 中react setInterval

IT技术 reactjs react-hooks settimeout setinterval use-effect
2021-05-22 07:15:32

我想在第一次触发时运行一个延迟时间间隔。我怎样才能用 useEffect 做到这一点?由于语法的原因,我发现很难实现我想要做的

区间函数

  useEffect(()=>{
    const timer = setInterval(() => {
      //do something here
      return ()=> clearInterval(timer)
    }, 1000);
  },[/*dependency*/])

延迟功能

useEffect(() => {
    setTimeout(() => {
//I want to run the interval here, but it will only run once 
//because of no dependencies. If i populate the dependencies, 
//setTimeout will run more than once.
}, Math.random() * 1000);
  }, []);

当然它可以以某种方式实现......

4个回答

入门

考虑解决组件的问题并编写小块。这里我们有一个useInterval自定义钩子,它严格定义setInterval了程序部分。我添加了一些console.log行,以便我们可以观察效果-

// rough draft
// read on to make sure we get all the parts right
function useInterval (f, delay)
{ const [timer, setTimer] =
    useState(null)
  
  const start = () =>
  { if (timer) return
    console.log("started")
    setTimer(setInterval(f, delay))
  }
  
  const stop = () =>
  { if (!timer) return
    console.log("stopped", timer)
    setTimer(clearInterval(timer))
  }
    
  useEffect(() => stop, [])
  
  return [start, stop, timer != null]
}

现在,当我们编写时,MyComp我们可以处理setTimeout程序一部分——

function MyComp (props)
{ const [counter, setCounter] =
    useState(0)
    
  const [start, stop, running] =
    useInterval(_ => setCounter(x => x + 1), 1000) // first try
  
  return <div>
    {counter}
    <button
      onClick={start}
      disabled={running}
      children="Start"
    />
    <button
      onClick={stop}
      disabled={!running}
      children="Stop"
    />
  </div>
}

现在我们可以useInterval在我们程序的各个部分中,并且每个部分都可以不同地使用。启动、停止和清理的所有逻辑都很好地封装在钩子中。

这是一个演示,您可以运行以查看它的工作情况-


做对

我们希望确保我们的useInterval钩子不会在我们的计时器停止或我们的组件被移除后让任何定时函数运行。让我们在一个更严格的例子中测试它们,我们可以添加/删除许多计时器并随时启动/停止它们 -

添加/删除计时器

一些根本性的改变是必要的useInterval——

function useInterval (f, delay = 1000)
{ const [busy, setBusy] = useState(0)
  
  useEffect(() => {
    // start
    if (!busy) return
    setBusy(true)
    const t = setInterval(f, delay)
    // stop
    return () => {
      setBusy(false)
      clearInterval(t)
    }
  }, [busy, delay])
  
  return [
    _ => setBusy(true),  // start
    _ => setBusy(false), // stop
    busy                 // isBusy
  ]
}

useIntervalMyTimer组件中使用很直观。MyTimer不需要对间隔进行任何类型的清理。清理由自动处理useInterval-

function MyTimer ({ delay = 1000, auto = true, ... props })
{ const [counter, setCounter] =
    useState(0)
    
  const [start, stop, busy] =
    useInterval(_ => {
      console.log("tick", Date.now()) // <-- for demo
      setCounter(x => x + 1)
    }, delay)

  useEffect(() => {
    console.log("delaying...") // <-- for demo
    setTimeout(() => {
      console.log("starting...") // <-- for demo
      auto && start()
    }, 2000)
  }, [])
  
  return <span>
    {counter}
    <button onClick={start} disabled={busy} children="Start" />
    <button onClick={stop} disabled={!busy} children="Stop" />
  </span>
}

Main组件没有做任何特别的事情。它只是管理MyTimer组件的数组状态不需要特定于定时器的代码或清理 -

const append = (a = [], x = null) =>
  [ ...a, x ]
  
const remove = (a = [], x = null) =>
{ const pos = a.findIndex(q => q === x)
  if (pos < 0) return a
  return [ ...a.slice(0, pos), ...a.slice(pos + 1) ]
}

function Main ()
{ const [timers, setTimers] = useState([])
  
  const addTimer = () =>
    setTimers(r => append(r, <MyTimer />))
    
  const destroyTimer = c => () =>
    setTimers(r => remove(r, c))
  
  return <main>
    <button
      onClick={addTimer}
      children="Add Timer"
    />
    { timers.map((c, key) =>
      <div key={key}>
        {c}
        <button
          onClick={destroyTimer(c)} 
          children="Destroy"
        />
      </div>
    )}
  </main>
}

展开下面的代码段以查看useInterval在您自己的浏览器中的工作情况。本演示推荐使用全屏模式 -


进阶

让我们想象一个更复杂的useInterval场景,其中定时函数fdelay可以改变 -

高级计时器

function useInterval (f, delay = 1000)
{ const [busy, setBusy] = // ...
  const interval = useRef(f)

  useEffect(() => {
    interval.current = f
  }, [f])
  
  useEffect(() => {
    // start
    // ...
    const t =
      setInterval(_ => interval.current(), delay)
      
    // stop
    // ...
  }, [busy, delay])
  
  return // ...
}

现在我们可以编辑MyTimer添加doublerturbo状态 -

function MyTimer ({ delay = 1000, auto = true, ... props })
{ const [counter, setCounter] = useState(0)
  
  const [doubler, setDoubler] = useState(false) // <--
  const [turbo, setTurbo] = useState(false)     // <--
  
  const [start, stop, busy] =
    useInterval
      ( doubler   // <-- doubler changes which f is run
          ? _ => setCounter(x => x * 2)
          : _ => setCounter(x => x + 1)
      , turbo     // <-- turbo changes delay
          ? Math.floor(delay / 2)
          : delay
      )

  // ...

然后我们添加一个doubleturbo按钮 -

  // ...
  const toggleTurbo = () =>
    setTurbo(t => !t)
    
  const toggleDoubler = () =>
    setDoubler(t => !t)
  
  return <span>
    {counter}
    {/* start button ... */}
    <button
      onClick={toggleDoubler}  // <--
      disabled={!busy}
      children={`Doubler: ${doubler ? "ON" : "OFF"}`}
    />
    <button
      onClick={toggleTurbo}    // <--
      disabled={!busy}
      children={`Turbo: ${turbo ? "ON" : "OFF"}`}
    />
    {/* stop button ... */}
  </span>
}

展开下面的代码片段以在您自己的浏览器中运行高级计时器演示 -

我认为您要做的是:

const DelayTimer = props => {
  const [value, setvalue] = React.useState("initial");
  const [counter, setcounter] = React.useState(0);

  React.useEffect(() => {
    let timer;
    setTimeout(() => {
      setvalue("delayed value");
      timer = setInterval(() => {
        setcounter(c => c + 1);
      }, 1000);
    }, 2000);
    return () => clearInterval(timer);
  }, []);

  return (
    <div>
      Value:{value} | counter:{counter}
    </div>
  );
};

// Render it
ReactDOM.render(<DelayTimer />, document.getElementById("react"));
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script></script>

如果您尝试使用setIntervalinside useEffect,我认为您将顺序调高了一点,应该是这样的

const INTERVAL_DELAY = 1000
useEffect(() => {
  const interval = setInterval(() => {
  /* do repeated stuff */
  }, INTERVAL_DELAY)
  return () => clearInterval(interval)
})

间隔将在延迟后开始,因此如果您希望 X 秒的间隔延迟在 Y 秒后开始,则必须实际使用 setTimeout 中的延迟作为 Y - X

const INITIAL_DELAY = 10000
const INTERVAL_DELAY = 5000

useEffect(() => {
  let interval
  setTimeout(() => {
    const interval = setInterval(() => {
    /* do repeated stuff */
    }, INTERVAL_DELAY)
  }, INITIAL_DELAY - INTERVAL_DELAY)
  return () => clearInterval(interval)
})

这是您想要达到的目标吗?useEffect 上的空数组告诉它在元素被渲染后将运行此代码

const {useState, useEffect} = React;
// Example stateless functional component
const SFC = props => {
  
  const [value,setvalue] = useState('initial')
  const [counter,setcounter] = useState(0)
  

  useEffect(() => {
    const timer = setInterval(() => {
    setvalue('delayed value')
    setcounter(counter+1)
    clearInterval(timer)
    }, 2000);
  }, []);
  
  return(<div>
          Value:{value} | counter:{counter}
         </div>)
};

// Render it
ReactDOM.render(
  <SFC/>,
  document.getElementById("react")
);
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script></script>