使用setTimeout延迟反应useEffect中的setInterval

时间:2020-05-23 12:08:27

标签: reactjs react-hooks settimeout setinterval use-effect

我想在它第一次触发时运行一个有延迟的间隔。我该如何使用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 个答案:

答案 0 :(得分:1)

我认为您要这样做的是

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>

答案 1 :(得分:1)

入门

请考虑整理组件的问题并编写小片段。这里我们有一个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 at useInterval

  useEffect(() => {
    console.log("delaying...")
    setTimeout(() => {
      console.log("starting...")
      !running && start()
    }, 2000)
  }, [])

  return <div>
    {counter}
    <button
      onClick={start}
      disabled={running}
      children="Start"
    />
    <button
      onClick={stop}
      disabled={!running}
      children="Stop"
    />
  </div>
}

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

这是一个演示,您可以运行以查看其工作原理-

const { useState, useEffect } = React

const useInterval = (f, delay) =>
{ const [timer, setTimer] = useState(undefined)
  
  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]
}
  
const MyComp = props =>
{ const [counter, setCounter] =
    useState(0)
    
  const [start, stop, running] =
    useInterval(_ => setCounter(x => x + 1), 1000)

  useEffect(() => {
    console.log("delaying...")
    setTimeout(() => {
      console.log("starting...")
      !running && start()
    }, 2000)
  }, [])
  
  return <div>
    {counter}
    <button
      onClick={start}
      disabled={running}
      children="Start"
    />
    <button
      onClick={stop}
      disabled={!running}
      children="Stop"
    />
  </div>
};


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


正确处理

如果要停止计时器或移除组件后,我们想确保useInterval钩子不会让任何定时功能运行。让我们在更严格的示例中对其进行测试,在该示例中,我们可以添加/删除许多计时器并随时启动/停止-

add/remove timers

需要对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
  ]
}

useInterval组件中使用MyTimer很直观。不需要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在您自己的浏览器中的运行情况。建议此演示使用全屏模式-

const { useState, useEffect } = React

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 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
  ]
}

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

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

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

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


进阶

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

advanced timers

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
      )

  // ...

然后我们添加一个 double turbo 按钮-

  // ...
  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 { useState, useEffect, useRef, useCallback } = React

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 useInterval (f, delay = 1000)
{ const interval = useRef(f)
  const [busy, setBusy] = useState(0)
  
  useEffect(() => {
    interval.current = f
  }, [f])
  
  useEffect(() => {
    // start
    if (!busy) return
    setBusy(true)
    const t =
      setInterval(_ => interval.current(), delay)
      
    // stop
    return () => {
      setBusy(false)
      clearInterval(t)
    }
  }, [busy, delay])
  
  return [
    _ => setBusy(true),  // start
    _ => setBusy(false), // stop
    busy                 // isBusy
  ]
}

function MyTimer ({ delay = 1000, ... props })
{ const [counter, setCounter] =
    useState(0)
  
  const [doubler, setDoubler] = useState(false)
  const [turbo, setTurbo] = useState(false)
  
  const [start, stop, busy] =
    useInterval
      ( doubler
          ? _ => setCounter(x => x * 2)
          : _ => setCounter(x => x + 1)
      , turbo
          ? Math.floor(delay / 2)
          : delay
      )
      
  const toggleTurbo = () =>
    setTurbo(t => !t)
    
  const toggleDoubler = () =>
    setDoubler(t => !t)
  
  return <span>
    {counter}
    <button
      onClick={start}
      disabled={busy}
      children="Start"
    />
    <button
      onClick={toggleDoubler}
      disabled={!busy}
      children={`Doubler: ${doubler ? "ON" : "OFF"}`}
    />
    <button
      onClick={toggleTurbo}
      disabled={!busy}
      children={`Turbo: ${turbo ? "ON" : "OFF"}`}
    />
    <button
      onClick={stop}
      disabled={!busy}
      children="Stop"
    />
  </span>
}

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

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

答案 2 :(得分:0)

如果您尝试在setInterval中使用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)
})

答案 3 :(得分:-1)

这是您要实现的目标吗? 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>