Мой таймер в Reactjs работает некорректно с задержкой...
Я заметил, что со временем мой таймер начинает отставать, если я переключаюсь на другую вкладку или окно браузера. Почему это происходит и как это исправить?
import React, { Component } from 'react';
class Test extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0
}
}
componentDidMount() {
this.timer = setInterval(() => {
this.setState((prevState) => ({ counter: prevState.counter > 0 ? prevState.counter - 1 : 0 }));
}, 1000);
}
componentDidUpdate() {
var target_date = new Date();
target_date.setHours(23,59,59);
var current_date = new Date();
current_date.getHours();
var counter = (target_date.getTime() - current_date.getTime()) / 1000;
if (this.state.counter === 0) {
this.setState({ counter: counter });
}
}
componentWillUnmount() {
clearInterval(this.timer);
}
render() {
const padTime = time => {
return String(time).length === 1 ? `0${time}` : `${time}`;
};
const format = time => {
const hours = Math.floor(time / (60 * 60));
const minutes = Math.floor(time % (60 * 60) / 60);
const seconds = time % 60;
return `${padTime(hours)}:${padTime(minutes)}:${padTime(seconds)}`;
};
return (
<div>{format(this.state.counter)}</div>
);
}
}
export default Test;
Помогите пожалуйста мне!
Это не связано с реакцией, это поведение браузера, браузер будет пытаться сэкономить вычислительную мощность, когда он не сфокусирован. Наверное, мог бы увидеть вот этот https://stackoverflow.com/a/6032591/649322
Не уверен, что вы подразумеваете под «задержкой», но если вы хотите быть максимально точным, вы можете заменить setInterval
на setTimeout
и рассчитать точное время следующего тика.
Например:
constructor(props) {
super(props);
this.timer = undefined;
this.prevTime = 0;
}
componentDidMount() {
const INTERVAL = 1000;
function update() {
console.info('do something');
const now = Date.now();
const delay = now - this.prevTime - INTERVAL;
const next = INTERVAL - diff;
this.setTimeout(update, next);
this.prevTime = now;
console.info('debug', Math.floor(now / 1000), delay, next);
}
update(); // fire the update
}
componentWillUnmount() {
clearTimeout(this.timer);
}
setTimeout()
и setInterval()
не всегда вызывают функцию обратного вызова вовремя. Его можно вызывать позже, если JavaScript выполняет другие действия, и/или его можно вызывать реже, если вкладка не сфокусирована.
Решение довольно простое, вместо использования counter
для отслеживания прошедших секунд. Вы должны сохранить контрольную точку start
. Затем при рендеринге вы можете вычислить counter
, сравнив start
с текущим временем.
Вот пример использования интервала в 1 мс для лучшего отображения разницы.
const { useState, useEffect } = React;
// Does NOT correctly keep track of time.
function TimerA() {
const [counter, setCounter] = useState(0);
useEffect(() => {
const intervalID = setInterval(() => setCounter(counter => counter + 1), 1);
return () => clearInterval(intervalID);
}, []);
return (
<div>
I will lag behind at some point.<br />
Milliseconds passed: {counter}
</div>
);
}
// Does correctly keep track of time.
function TimerB() {
const [start] = useState(Date.now());
const [now, setNow] = useState(start);
const counter = now - start;
useEffect(() => {
const intervalID = setInterval(() => setNow(Date.now()), 1);
return () => clearInterval(intervalID);
}, []);
return (
<div>
I will always stay on time.<br />
Milliseconds passed: {counter}
</div>
);
}
ReactDOM.render(
<div>
<TimerA />
<hr />
<TimerB />
</div>,
document.querySelector("#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>
Don't use 1ms in production code. If the callback takes more time to execute than the interval (which it probably does in the above snippet). Then the callback queue will only grow until eventually something gives.
Если вы используете компоненты класса, вам не нужно хранить now
в состоянии. Вместо этого это может быть обычная переменная const now = Date.now()
, и вы должны использовать setInterval(() => this.forceUpdate(), ...)
. Единственная причина, по которой я храню now
в состоянии, заключается в том, что функциональные компоненты не имеют доступа к forceUpdate()
.
Теперь, когда вы, надеюсь, понимаете проблему и решение, мы можем вернуться к коду вопроса.
Поскольку вы рассчитываете время до полуночи, нет смысла хранить start
. Вместо этого вы должны рассчитать следующую полночь и использовать ее в качестве точки отсчета. Затем вычислил время между сегодняшним днем и полуночью. Оба должны быть сделаны для каждого рендера.
<script type = "text/babel">
// time constants
const MILLISECOND = 1, MILLISECONDS = MILLISECOND;
const SECOND = 1000*MILLISECONDS, SECONDS = SECOND;
const MINUTE = 60*SECONDS, MINUTES = MINUTE;
const HOUR = 60*MINUTES, HOURS = HOUR;
const DAY = 24*HOURS, DAYS = DAY;
function nextMidnight() {
const midnight = new Date();
midnight.setHours(0, 0, 0, 0);
return midnight.valueOf() + (1*DAY);
}
function Timer({ ms }) {
const hours = Math.floor(ms / (1*HOUR));
const minutes = Math.floor(ms % (1*HOUR) / (1*MINUTE));
const seconds = Math.floor(ms % (1*MINUTE) / (1*SECOND));
const pad = (n) => n.toString().padStart(2, "0");
return <>{pad(hours)}:{pad(minutes)}:{pad(seconds)}</>;
}
class Test extends React.Component {
componentDidMount() {
this.intervalID = setInterval(() => this.forceUpdate(), 1*SECOND);
}
componentWillUnmount() {
clearInterval(this.intervalID);
}
render() {
const msUntilMidnight = nextMidnight() - Date.now();
return (
<div>
<Timer ms = {msUntilMidnight} />
</div>
);
}
}
ReactDOM.render(<Test />, document.querySelector("#root"));
</script>
<div id = "root"></div>
<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>
<script src = "https://unpkg.com/@babel/standalone@7/babel.min.js"></script>
@Vlad Влад Я обновил ответ рефакторинговой версией кода в вопросе.
Спасибо за ваш ответ. Да, я использую компонент класса. Вы можете исправить мой код с помощью forceUpdate()?