使用 setTimeout 延迟在 useEffect 中反应 setInterval [英] React setInterval in useEffect with setTimeout delay

查看:38
本文介绍了使用 setTimeout 延迟在 useEffect 中反应 setInterval的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

区间函数

 useEffect(()=>{const timer = setInterval(() => {//在这里做点什么返回 ()=>清除间隔(定时器)}, 1000);},[/*依赖*/])

延迟函数

useEffect(() => {setTimeout(() => {//我想在这里运行间隔,但它只会运行一次//因为没有依赖.如果我填充依赖项,//setTimeout 将运行多次.}, Math.random() * 1000);}, []);

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

解决方案

入门

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

//草稿//继续阅读以确保所有部分都正确函数 useInterval (f, 延迟){ const [定时器,setTimer] =使用状态(空)const start = () =>{ 如果(定时器)返回console.log(开始")setTimer(setInterval(f, delay))}const stop = () =>{ if (!timer) 返回console.log(停止",计时器)setTimer(clearInterval(timer))}useEffect(() => stop, [])返回 [开始,停止,计时器!= null]}

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

function MyComp (props){ const [计数器,设置计数器] =使用状态(0)const [开始,停止,运行] =useInterval(_ => setCounter(x => x + 1), 1000)//第一次尝试返回 

{柜台}<按钮onClick={开始}禁用={运行}儿童=开始"/><按钮onClick={停止}禁用={!running}儿童=停止"/>

}

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

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

const { useState, useEffect } = Reactconst useInterval = (f, delay) =>{ const [定时器,setTimer] =使用状态(未定义)const start = () =>{ 如果(定时器)返回console.log("开始")setTimer(setInterval(f, delay))}const stop = () =>{ if (!timer) 返回控制台日志(停止",计时器)setTimer(clearInterval(timer))}useEffect(() => stop, [])返回 [开始,停止,计时器!= null]}const MyComp = 道具 =>{ const [计数器,设置计数器] =使用状态(0)const [开始,停止,运行] =useInterval(_ => setCounter(x => x + 1), 1000)返回 

{柜台}<按钮onClick={开始}禁用={运行}孩子=开始"/><按钮onClick={停止}禁用={!running}儿童=停止"/>

};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></脚本>


正确处理

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

需要对 useInterval 进行一些根本性的更改 -

函数 useInterval (f, delay = 1000){ const [busy, setBusy] = useState(0)useEffect(() => {//开始if (!busy) 返回设置忙(真)const t = setInterval(f, 延迟)//停止返回 () =>{设置忙(假)清除间隔(t)}}, [忙,延迟])返回 [_ =>setBusy(true),//开始_ =>setBusy(false),//停止忙碌//isBusy]}

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

自动处理

function MyTimer ({ delay = 1000, auto = true, ... props }){ const [计数器,设置计数器] =使用状态(0)const [开始,停止,忙碌] =useInterval(_ => {console.log("tick", Date.now())//<-- 用于演示设置计数器(x => x + 1)}, 延迟)useEffect(() => {console.log("delaying...")//<-- 用于演示setTimeout(() => {console.log("starting...")//<-- 用于演示汽车&&开始()}, 2000)}, [])返回 <span>{柜台}<button onClick={start} disabled={busy} children=Start"/><button onClick={stop} disabled={!busy} children=停止"/></span>}

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

const append = (a = [], x = null) =>[ ...a, x ]const remove = (a = [], x = null) =>{ const pos = a.findIndex(q => q === x)如果 (pos <0) 返回一个返回 [ ...a.slice(0, pos), ...a.slice(pos + 1) ]}函数主(){ const [timers, setTimers] = useState([])const addTimer = () =>setTimers(r => append(r, ))const destroyTimer = c =>() =>setTimers(r => remove(r, c))返回 <main><按钮onClick={addTimer}儿童=添加计时器"/>{ timers.map((c, key) =><div key={key}>{C}<按钮onClick={destroyTimer(c)}儿童=破坏"/>

)}</main>}

展开下面的代码段以查看 useInterval 在您自己的浏览器中的运行情况.本演示建议使用全屏模式 -

const { useState, useEffect } = Reactconst append = (a = [], x = null) =>[ ...a, x ]const remove = (a = [], x = null) =>{ const pos = a.findIndex(q => q === x)如果 (pos <0) 返回一个返回 [ ...a.slice(0, pos), ...a.slice(pos + 1) ]}函数 useInterval (f, delay = 1000){ const [busy, setBusy] = useState(0)useEffect(() => {//开始if (!busy) 返回设置忙(真)const t = setInterval(f, 延迟)//停止返回 () =>{设置忙(假)清除间隔(t)}}, [忙,延迟])返回 [_ =>setBusy(true),//开始_ =>setBusy(false),//停止忙碌//isBusy]}函数 MyTimer ({ delay = 1000, auto = true, ... props }){ const [计数器,设置计数器] =使用状态(0)const [开始,停止,忙碌] =useInterval(_ => {console.log("tick", Date.now())设置计数器(x => x + 1)}, 延迟)useEffect(() => {console.log("延迟...")setTimeout(() => {console.log("开始...")汽车&&开始()}, 2000)}, [])返回 <span>{柜台}<按钮onClick={开始}禁用={忙}孩子=开始"/><按钮onClick={停止}禁用={!busy}儿童=停止"/></span>}函数主(){ const [timers, setTimers] = useState([])const addTimer = () =>setTimers(r => append(r, ))const destroyTimer = c =>() =>setTimers(r => remove(r, c))返回 <main><p>以扩展模式运行.打开您的开发者控制台</p><按钮onClick={addTimer}孩子=添加计时器"/>{ timers.map((c, key) =><div key={key}>{C}<按钮onClick={destroyTimer(c)}孩子=摧毁"/>

)}</main>}ReactDOM.render( <主/>, 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></脚本>


进阶

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

函数 useInterval (f, delay = 1000){ const [busy, setBusy] =//...const 间隔 = useRef(f)useEffect(() => {间隔.电流 = f}, [F])useEffect(() => {//开始//...常数 t =setInterval(_ => interval.current(), delay)//停止//...}, [忙,延迟])返回//...}

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

function MyTimer ({ delay = 1000, auto = true, ... props }){ const [计数器,设置计数器] = useState(0)const [doubler, setDoubler] = useState(false)//<--const [turbo, setTurbo] = useState(false)//<--const [开始,停止,忙碌] =使用间隔( doubler//<-- doubler 改变运行的 f?_ =>设置计数器(x => x * 2): _ =>设置计数器(x => x + 1), turbo//<-- turbo 改变延迟?Math.floor(延迟/2): 延迟)//...

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

//...const toggleTurbo = () =>setTurbo(t => !t)const toggleDouble = () =>setDoubler(t => !t)返回 <span>{柜台}{/* 开始按钮 ... */}<按钮onClick={toggleDoubler}//<--禁用={!busy}儿童={`倍频器:${倍频器?开":关闭"}`}/><按钮onClick={toggleTurbo}//<--禁用={!busy}孩子们={`涡轮增压:${涡轮增压?开":关闭"}`}/>{/* 停止按钮 ... */}</span>}

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

const { useState, useEffect, useRef, useCallback } = Reactconst append = (a = [], x = null) =>[ ...a, x ]const remove = (a = [], x = null) =>{ const pos = a.findIndex(q => q === x)如果 (pos <0) 返回一个返回 [ ...a.slice(0, pos), ...a.slice(pos + 1) ]}函数 useInterval (f, delay = 1000){ const 间隔 = useRef(f)const [忙,setBusy] = useState(0)useEffect(() => {间隔.电流 = f}, [F])useEffect(() => {//开始if (!busy) 返回设置忙(真)常数 t =setInterval(_ => interval.current(), delay)//停止返回 () =>{设置忙(假)清除间隔(t)}}, [忙,延迟])返回 [_ =>setBusy(true),//开始_ =>setBusy(false),//停止忙碌//isBusy]}函数 MyTimer ({ delay = 1000, ... props }){ const [计数器,设置计数器] =使用状态(0)const [doubler, setDoubler] = useState(false)const [turbo, setTurbo] = useState(false)const [开始,停止,忙碌] =使用间隔( 倍增器?_ =>设置计数器(x => x * 2): _ =>设置计数器(x => x + 1), 涡轮增压?Math.floor(延迟/2): 延迟)const toggleTurbo = () =>setTurbo(t => !t)const toggleDouble = () =>setDoubler(t => !t)返回 <span>{柜台}<按钮onClick={开始}禁用={忙}孩子=开始"/><按钮onClick={toggleDoubler}禁用={!busy}儿童={`倍频器:${倍频器?开":关"}`}/><按钮onClick={toggleTurbo}禁用={!busy}孩子们={`涡轮增压:${涡轮增压?开":关"}`}/><按钮onClick={停止}禁用={!busy}儿童=停止"/></span>}函数主(){ const [timers, setTimers] = useState([])const addTimer = () =>setTimers(r => append(r, ))const destroyTimer = c =>() =>setTimers(r => remove(r, c))返回 <main><p>以扩展模式运行.打开您的开发者控制台</p><按钮onClick={addTimer}孩子=添加计时器"/>{ timers.map((c, key) =><div key={key}>{C}<按钮onClick={destroyTimer(c)}孩子=摧毁"/>

)}</main>}ReactDOM.render( <主/>, 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></脚本>

I want to run an interval with a delay for the first time it fires. How can I do this with useEffect? Because of the syntax I've found it difficult to achieve what I want to do

The interval function

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

The delay function

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);
  }, []);

Sure it is achievable somehow...

解决方案

getting started

Consider detangling the concerns of your component and writing small pieces. Here we have a useInterval custom hook which strictly defines the setInterval portion of the program. I added some console.log lines so we can observe the effects -

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

Now when we write MyComp we can handle the setTimeout portion of the program -

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

Now we can useInterval in various parts of our program, and each one can be used differently. All the logic for the start, stop and cleanup is nicely encapsulated in the hook.

Here's a demo you can run to see it working -

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


getting it right

We want to make sure our useInterval hook doesn't leave any timed functions running if our timer are stopped or after our components are removed. Let's test them out in a more rigorous example where we can add/remove many timers and start/stop them at any time -

A few fundamental changes were necessary to make to 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
  ]
}

Using useInterval in MyTimer component is intuitive. MyTimer is not required to do any sort of cleanup of the interval. Cleanup is automatically handled by 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>
}

The Main component doesn't do anything special. It just manages an array state of MyTimer components. No timer-specific code or clean up is required -

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

Expand the snippet below to see useInterval working in your own browser. Fullscreen mode is recommended for this demo -

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>


getting advanced

Let's imagine an even more complex useInterval scenario where the timed function, f, and the delay can change -

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 // ...
}

Now we can edit MyTimer to add the doubler and turbo state -

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
      )

  // ...

Then we add a double and turbo button -

  // ...
  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>
}

Expand the snippet below to run the advanced timer demo in your own browser -

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>

这篇关于使用 setTimeout 延迟在 useEffect 中反应 setInterval的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
其他开发最新文章
热门教程
热门工具
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆