Мой таймер в Reactjs работает некорректно с задержкой

Мой таймер в 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;

Помогите пожалуйста мне!

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
0
52
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Это не связано с реакцией, это поведение браузера, браузер будет пытаться сэкономить вычислительную мощность, когда он не сфокусирован. Наверное, мог бы увидеть вот этот 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>

Спасибо за ваш ответ. Да, я использую компонент класса. Вы можете исправить мой код с помощью forceUpdate()?

Vlad 06.05.2022 20:22

@Vlad Влад Я обновил ответ рефакторинговой версией кода в вопросе.

3limin4t0r 06.05.2022 21:32

Другие вопросы по теме