React中的倒计时计时器

92

我曾见过很多JavaScript倒计时器,并想要在React中让一个工作。

我借用了我在网上找到的这个函数:

secondsToTime(secs){
    let hours = Math.floor(secs / (60 * 60));

    let divisor_for_minutes = secs % (60 * 60);
    let minutes = Math.floor(divisor_for_minutes / 60);

    let divisor_for_seconds = divisor_for_minutes % 60;
    let seconds = Math.ceil(divisor_for_seconds);

    let obj = {
        "h": hours,
        "m": minutes,
        "s": seconds
    };
    return obj;
  };

然后我自己编写了这段代码

  initiateTimer = () => {
    let timeLeftVar = this.secondsToTime(60);
    this.setState({ timeLeft: timeLeftVar })
  };

  startTimer = () => {
    let interval = setInterval(this.timer, 1000);
    this.setState({ interval: interval });
  };

  timer = () => {
    if (this.state.timeLeft >0){
      this.setState({ timeLeft: this.state.timeLeft -1 });
    }
    else {
      clearInterval(this.state.interval);
      //this.postToSlack();
    }
  };

当前,点击时屏幕上会显示时间: 剩余时间:1分钟: 0秒

但它没有把它减少到剩余时间:0分钟:59秒,然后是剩余时间:0分钟:58秒等等。

我想我需要使用不同的参数再次调用该函数。我该如何实现?

编辑:我忘记说了,我希望能够将秒转换为分钟和秒


1
React文档示例之一是一个可以自我更新的时钟,看起来非常有用... - T.J. Crowder
@T.J.Crowder 这有点帮助。他们只是获取了时间,因为可以通过componentDidMount返回它,而我只想从起始位置提取秒和分钟。 - The worm
也许您可以在问题中使用 Stack Snippets 放置一个可运行的 [mcve],它支持 React 和 JSX,这样我们就可以看到问题的实际情况。 - T.J. Crowder
@T.J.Crowder发现在JSfiddle中创建一个非常困难,因为我在许多文件中使用了许多组件和许多道具。 - The worm
@T.J.Crowder,对于你来说,这个问题有什么意义?(看看我是否可以为解释不太好的事情增加更多的知识) - The worm
@T.J.Crowder 对于垃圾邮件我很抱歉。我需要更多的是一种将秒转换为秒/分钟的方法,例如10 -> 00:10或65 -> 01:05在React中。基本上是一种美观的方式来格式化我的状态。 - The worm
20个回答

97

您需要每秒钟使用剩余的秒数来调用setState(每次调用间隔时)。以下是一个示例:

class Example extends React.Component {
  constructor() {
    super();
    this.state = { time: {}, seconds: 5 };
    this.timer = 0;
    this.startTimer = this.startTimer.bind(this);
    this.countDown = this.countDown.bind(this);
  }

  secondsToTime(secs){
    let hours = Math.floor(secs / (60 * 60));

    let divisor_for_minutes = secs % (60 * 60);
    let minutes = Math.floor(divisor_for_minutes / 60);

    let divisor_for_seconds = divisor_for_minutes % 60;
    let seconds = Math.ceil(divisor_for_seconds);

    let obj = {
      "h": hours,
      "m": minutes,
      "s": seconds
    };
    return obj;
  }

  componentDidMount() {
    let timeLeftVar = this.secondsToTime(this.state.seconds);
    this.setState({ time: timeLeftVar });
  }

  startTimer() {
    if (this.timer == 0 && this.state.seconds > 0) {
      this.timer = setInterval(this.countDown, 1000);
    }
  }

  countDown() {
    // Remove one second, set state so a re-render happens.
    let seconds = this.state.seconds - 1;
    this.setState({
      time: this.secondsToTime(seconds),
      seconds: seconds,
    });
    
    // Check if we're at zero.
    if (seconds == 0) { 
      clearInterval(this.timer);
    }
  }

  render() {
    return(
      <div>
        <button onClick={this.startTimer}>Start</button>
        m: {this.state.time.m} s: {this.state.time.s}
      </div>
    );
  }
}

ReactDOM.render(<Example/>, document.getElementById('View'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="View"></div>


1
这看起来不错。一个问题是它不会在0处停止,而是继续到负数?修复它,我就接受了 ;) - The worm
2
好的,这与您最初的代码类似。检查是否还有剩余时间,然后执行 clearInterval。我已更新我的答案。 - Fabian Schultz
1
您还可以进行更多的优化,比如重置计时器、暂停等等,但问题的重点是如何倒计时并在渲染中反映出来。 - Fabian Schultz
1
干杯,我的代码还在不知道什么奇怪的原因下出现了负数。我甚至调用了console.log(seconds),结果显示为0,所以必须进一步调试。 - The worm
3
@FabianSchultz,你的解决方案太棒了。对于我构建倒计时计时器组件和入门非常有帮助。代码非常干净利落。继续保持出色的工作! - Ravindra Ranwala
显示剩余13条评论

69

这里是一个使用hooks和Timer组件的解决方案,我正在使用hooks复制相同的逻辑。

import React from 'react'
import { useState, useEffect } from 'react';

const Timer = (props:any) => {
    const {initialMinute = 0,initialSeconds = 0} = props;
    const [ minutes, setMinutes ] = useState(initialMinute);
    const [seconds, setSeconds ] =  useState(initialSeconds);
    useEffect(()=>{
    let myInterval = setInterval(() => {
            if (seconds > 0) {
                setSeconds(seconds - 1);
            }
            if (seconds === 0) {
                if (minutes === 0) {
                    clearInterval(myInterval)
                } else {
                    setMinutes(minutes - 1);
                    setSeconds(59);
                }
            } 
        }, 1000)
        return ()=> {
            clearInterval(myInterval);
          };
    });

    return (
        <div>
        { minutes === 0 && seconds === 0
            ? null
            : <h1> {minutes}:{seconds < 10 ?  `0${seconds}` : seconds}</h1> 
        }
        </div>
    )
}

export default Timer;

4
你每一秒钟都初始化和清除间隔,我认为将空数组作为useEffect的依赖关系会更好。 - Israel kusayev
3
@Israelkusayev 如果我添加[]数组,它只会触发一次,我需要添加[秒,分钟],这将再次起作用 - Masood
2
不错的代码。但我认为应该使用setTimeout而不是setInterval。 - Ike Anya
1
始终传递一个依赖项数组。您可能需要重写您的逻辑。 - Sumit Wadhwa
1
当我点击一个按钮时,如何处理它,使计时器开始? - Mijanur Rahman
@MijanurRahman 在点击开始按钮时,你应该使用状态并设置分钟和秒钟的值。 - Masood

12

这里是一个简单的实现,使用hooks和@dan-abramov提供的useInterval实现。

import React, {useState, useEffect, useRef} from 'react'
import './styles.css'

const STATUS = {
  STARTED: 'Started',
  STOPPED: 'Stopped',
}

const INITIAL_COUNT = 120

export default function CountdownApp() {
  const [secondsRemaining, setSecondsRemaining] = useState(INITIAL_COUNT)
  const [status, setStatus] = useState(STATUS.STOPPED)

  const secondsToDisplay = secondsRemaining % 60
  const minutesRemaining = (secondsRemaining - secondsToDisplay) / 60
  const minutesToDisplay = minutesRemaining % 60
  const hoursToDisplay = (minutesRemaining - minutesToDisplay) / 60

  const handleStart = () => {
    setStatus(STATUS.STARTED)
  }
  const handleStop = () => {
    setStatus(STATUS.STOPPED)
  }
  const handleReset = () => {
    setStatus(STATUS.STOPPED)
    setSecondsRemaining(INITIAL_COUNT)
  }
  useInterval(
    () => {
      if (secondsRemaining > 0) {
        setSecondsRemaining(secondsRemaining - 1)
      } else {
        setStatus(STATUS.STOPPED)
      }
    },
    status === STATUS.STARTED ? 1000 : null,
    // passing null stops the interval
  )
  return (
    <div className="App">
      <h1>React Countdown Demo</h1>
      <button onClick={handleStart} type="button">
        Start
      </button>
      <button onClick={handleStop} type="button">
        Stop
      </button>
      <button onClick={handleReset} type="button">
        Reset
      </button>
      <div style={{padding: 20}}>
        {twoDigits(hoursToDisplay)}:{twoDigits(minutesToDisplay)}:
        {twoDigits(secondsToDisplay)}
      </div>
      <div>Status: {status}</div>
    </div>
  )
}

// source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
function useInterval(callback, delay) {
  const savedCallback = useRef()

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback
  }, [callback])

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current()
    }
    if (delay !== null) {
      let id = setInterval(tick, delay)
      return () => clearInterval(id)
    }
  }, [delay])
}

// https://dev59.com/znA75IYBdhLWcg3w-OZA#2998874
const twoDigits = (num) => String(num).padStart(2, '0')

以下是 CodeSandbox 实现:https://codesandbox.io/s/react-countdown-demo-gtr4u?file=/src/App.js


1
谢谢,帮了我很多!只是:不要在 setIntervalms 参数中使用 null - Guilherme Abacherli

10

使用Date.now()来显示倒计时的基本思想,而不是减去1,因为后者会随着时间的推移而漂移。

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      time: {
        hours: 0,
        minutes: 0,
        seconds: 0,
        milliseconds: 0,
      },
      duration: 2 * 60 * 1000,
      timer: null
    };
    this.startTimer = this.start.bind(this);
  }

  msToTime(duration) {
    let milliseconds = parseInt((duration % 1000));
    let seconds = Math.floor((duration / 1000) % 60);
    let minutes = Math.floor((duration / (1000 * 60)) % 60);
    let hours = Math.floor((duration / (1000 * 60 * 60)) % 24);

    hours = hours.toString().padStart(2, '0');
    minutes = minutes.toString().padStart(2, '0');
    seconds = seconds.toString().padStart(2, '0');
    milliseconds = milliseconds.toString().padStart(3, '0');

    return {
      hours,
      minutes,
      seconds,
      milliseconds
    };
  }

  componentDidMount() {}

  start() {
    if (!this.state.timer) {
      this.state.startTime = Date.now();
      this.timer = window.setInterval(() => this.run(), 10);
    }
  }

  run() {
    const diff = Date.now() - this.state.startTime;
    
    // If you want to count up
    // this.setState(() => ({
    //  time: this.msToTime(diff)
    // }));
    
    // count down
    let remaining = this.state.duration - diff;
    if (remaining < 0) {
      remaining = 0;
    }
    this.setState(() => ({
      time: this.msToTime(remaining)
    }));
    if (remaining === 0) {
      window.clearTimeout(this.timer);
      this.timer = null;
    }
  }

  render() {
    return ( <
      div >
      <
      button onClick = {
        this.startTimer
      } > Start < /button> {
        this.state.time.hours
      }: {
        this.state.time.minutes
      }: {
        this.state.time.seconds
      }. {
        this.state.time.milliseconds
      }:
      <
      /div>
    );
  }
}

ReactDOM.render( < Example / > , document.getElementById('View'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="View"></div>


1
这是一种更加稳定的方法。然而问题在于,暂停和继续有些更具挑战性。 - Tosh
1
@Tosh 不是很准确...你知道暂停时经过了多长时间,你要存储它。继续时,计算出差异并设置新的开始时间。 - epascarello

8

简单分辨率:

import React, { useState, useEffect } from "react";

const Timer = ({ delayResend = "180" }) => {
  const [delay, setDelay] = useState(+delayResend);
  const minutes = Math.floor(delay / 60);
  const seconds = Math.floor(delay % 60);
  useEffect(() => {
    const timer = setInterval(() => {
      setDelay(delay - 1);
    }, 1000);

    if (delay === 0) {
      clearInterval(timer);
    }

    return () => {
      clearInterval(timer);
    };
  });

  return (
    <>
      <span>
        {minutes}:{seconds}
      </span>
    </>
  );
};

export default Timer;

1
最简单的答案,谢谢。 - Sehrish Waheed

3

我也遇到了同样的问题,但我找到了一个npm包可用于倒计时。

  1. 安装这个包

    npm install react-countdown --save 或者

    yarn add react-countdown

  2. 将此包导入你的文件

    import Countdown from 'react-countdown';

  3. 在render方法中调用导入的“Countdown”,并传递一个日期

    <Countdown date={new Date('2021-09-26T10:05:29.896Z').getTime()}> 或者

    <Countdown date={new Date("Sat Sep 26 2021")}>

下面是一个示例:

import React from "react";
import ReactDOM from "react-dom";
import Countdown from "react-countdown";

// Random component
const Completionist = () => <span>You are good to go!</span>;

ReactDOM.render(
  <Countdown date={new Date('2021-09-26T10:05:29.896Z').getTime()}>
    <Completionist />
  </Countdown>,
  document.getElementById("root")
);

你可以在这里查看详细文档:https://www.npmjs.com/package/react-countdown

3
问题出在你的“this”值上。 定时器函数无法访问“state”属性,因为它运行在不同的上下文中。我建议你做如下处理:
...
startTimer = () => {
  let interval = setInterval(this.timer.bind(this), 1000);
  this.setState({ interval });
};

如您所见,我已经为您的计时器函数添加了一个“bind”方法。这允许在调用计时器时,它可以访问您React组件的相同“this”(这是在一般情况下使用JavaScript时的主要问题/改进)。

另一种选择是使用另一个箭头函数:

startTimer = () => {
  let interval = setInterval(() => this.timer(), 1000);
  this.setState({ interval });
};

啊,是的,我忘记绑定了。但这并没有解决我的主要问题,它还是没有倒计时? - The worm

3

用户输入的倒计时

界面截图 screenshot

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  constructor() {
    super();
    this.state = {
      hours: 0,
      minutes: 0,
      seconds:0
    }
    this.hoursInput = React.createRef();
    this.minutesInput= React.createRef();
    this.secondsInput = React.createRef();
  }

  inputHandler = (e) => {
    this.setState({[e.target.name]: e.target.value});
  }

  convertToSeconds = ( hours, minutes,seconds) => {
    return seconds + minutes * 60 + hours * 60 * 60;
  }

  startTimer = () => {
    this.timer = setInterval(this.countDown, 1000);
  }

  countDown = () => {
    const  { hours, minutes, seconds } = this.state;
    let c_seconds = this.convertToSeconds(hours, minutes, seconds);

    if(c_seconds) {

      // seconds change
      seconds ? this.setState({seconds: seconds-1}) : this.setState({seconds: 59});

      // minutes change
      if(c_seconds % 60 === 0 && minutes) {
        this.setState({minutes: minutes -1});
      }

      // when only hours entered
      if(!minutes && hours) {
        this.setState({minutes: 59});
      }

      // hours change
      if(c_seconds % 3600 === 0 && hours) {
        this.setState({hours: hours-1});
      }

    } else {
      clearInterval(this.timer);
    }
  }


  stopTimer = () => {
    clearInterval(this.timer);
  }

  resetTimer = () => {
    this.setState({
      hours: 0,
      minutes: 0,
      seconds: 0
    });
    this.hoursInput.current.value = 0;
    this.minutesInput.current.value = 0;
    this.secondsInput.current.value = 0;
  }


  render() {
    const { hours, minutes, seconds } = this.state;

    return (
      <div className="App">
         <h1 className="title"> (( React Countdown )) </h1>
         <div className="inputGroup">
            <h3>Hrs</h3>
            <input ref={this.hoursInput} type="number" placeholder={0}  name="hours"  onChange={this.inputHandler} />
            <h3>Min</h3>
            <input  ref={this.minutesInput} type="number"  placeholder={0}   name="minutes"  onChange={this.inputHandler} />
            <h3>Sec</h3>
            <input   ref={this.secondsInput} type="number"  placeholder={0}  name="seconds"  onChange={this.inputHandler} />
         </div>
         <div>
            <button onClick={this.startTimer} className="start">start</button>
            <button onClick={this.stopTimer}  className="stop">stop</button>
            <button onClick={this.resetTimer}  className="reset">reset</button>
         </div>
         <h1> Timer {hours}: {minutes} : {seconds} </h1>
      </div>

    );
  }
}

export default App;


你好,我该如何在时间间隔为零后调用一个函数? - Vamsee.

1

功能:

1)启动

2)重置

功能组件

import {useState, useCallback} from 'react';
const defaultCount = 10;
const intervalGap = 300;

const Counter = () => {
    const [timerCount, setTimerCount] = useState(defaultCount);
    
    const startTimerWrapper = useCallback((func)=>{
        let timeInterval: NodeJS.Timer;
        return () => {
            if(timeInterval) {
                clearInterval(timeInterval)
            }
            setTimerCount(defaultCount)
            timeInterval = setInterval(() => {
                func(timeInterval)
            }, intervalGap)
        }
    }, [])

    const timer = useCallback(startTimerWrapper((intervalfn: NodeJS.Timeout) => {
         setTimerCount((val) => {
            if(val === 0 ) {
                clearInterval(intervalfn);
                return val
            } 
            return val - 1
        })
    }), [])

    return <>
        <div> Counter App</div>
        <div> <button onClick={timer}>Start/Reset</button></div>
        <div> {timerCount}</div>
    </>
}
export default Counter;

如何引用 NodeJS.Timer?这给了我一个异常。 - AJAY KUMAR

1

以下是使用自定义钩子的简单实现:

import * as React from 'react';

// future time from where countdown will start decreasing
const futureTime = new Date( Date.now() + 5000 ).getTime(); // adding 5 seconds

export const useCountDown = (stop = false) => {

   const [time, setTime] = React.useState(
       futureTime - Date.now()
   );

   // ref to store interval which we can clear out later
   // when stop changes through parent component (component that uses this hook)
   // causing the useEffect callback to trigger again
   const intervalRef = React.useRef<NodeJS.Timer | null>(null);

   React.useEffect(() => {
       if (intervalRef.current) {
           clearInterval(intervalRef.current);
           intervalRef.current = null;
           return;
       }

       const interval = intervalRef.current = setInterval(() => {
          setTime(futureTime - Date.now());
       }, 1000);

      return () => clearInterval(interval);
   }, [stop]);

   return getReturnValues(time);
};

const getReturnValues = (countDown: number) => {
    const days = Math.floor(countDown / (1000 * 60 * 60 * 24));
    const hours = Math.floor(
      (countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
    );
    const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60));
    const seconds = Math.floor((countDown % (1000 * 60)) / 1000);

    return [days, hours, minutes, seconds];
};

使用此钩子的示例:

function App() {

    const [timerStopped, stopTimer] = React.useState(false);
    const [,hours,minutes,seconds]  = useCountDown(timerStopped);

   // to stop the interval
   if( !timerStopped && minutes + seconds <= 0 ) {
      stopTimer(true);
   }

   return (
     <div className="App">
       Time Left: {hours}:{minutes}:{seconds}
       { timerStopped ? (
           <h1>Time's up</h1>
        ) : null }
     </div>
   );
}

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