← Назад к вопросам

Как реализовать секундомер?

2.0 Middle🔥 142 комментариев
#Soft Skills и рабочие процессы

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как реализовать секундомер

Базовая реализация с setInterval

Простейший вариант — использовать setInterval и счётчик времени:

class Stopwatch {
  constructor() {
    this.seconds = 0;
    this.intervalId = null;
    this.isRunning = false;
  }

  start() {
    if (this.isRunning) return;
    this.isRunning = true;

    this.intervalId = setInterval(() => {
      this.seconds++;
      this.display();
    }, 1000); // каждую секунду
  }

  stop() {
    this.isRunning = false;
    clearInterval(this.intervalId);
  }

  reset() {
    this.seconds = 0;
    this.stop();
    this.display();
  }

  display() {
    const hours = Math.floor(this.seconds / 3600);
    const minutes = Math.floor((this.seconds % 3600) / 60);
    const secs = this.seconds % 60;

    const timeString = `${this.pad(hours)}:${this.pad(minutes)}:${this.pad(secs)}`;
    document.getElementById('display').textContent = timeString;
  }

  pad(num) {
    return num.toString().padStart(2, '0');
  }
}

// Использование
const stopwatch = new Stopwatch();
document.getElementById('start').addEventListener('click', () => stopwatch.start());
document.getElementById('stop').addEventListener('click', () => stopwatch.stop());
document.getElementById('reset').addEventListener('click', () => stopwatch.reset());

Проблема: неточность setInterval

setInterval может отклоняться из-за браузера и системы:

// Проблема: setInterval может задерживаться
setInterval(() => {
  seconds++; // Может быть не точно каждую секунду!
}, 1000);

// За 10 минут может ошибиться на несколько секунд

Лучший вариант: с performance.now()

Используй реальное время вместо счётчика:

class AccurateStopwatch {
  constructor() {
    this.startTime = null;
    this.elapsedTime = 0; // миллисекунды
    this.animationId = null;
    this.isRunning = false;
  }

  start() {
    if (this.isRunning) return;
    this.isRunning = true;
    this.startTime = performance.now();

    const update = () => {
      const currentTime = performance.now();
      this.elapsedTime += currentTime - this.startTime;
      this.startTime = currentTime;
      this.display();

      if (this.isRunning) {
        this.animationId = requestAnimationFrame(update);
      }
    };

    this.animationId = requestAnimationFrame(update);
  }

  stop() {
    this.isRunning = false;
    if (this.animationId) {
      cancelAnimationFrame(this.animationId);
    }
  }

  reset() {
    this.elapsedTime = 0;
    this.stop();
    this.display();
  }

  display() {
    const totalSeconds = Math.floor(this.elapsedTime / 1000);
    const milliseconds = Math.floor((this.elapsedTime % 1000) / 10); // сотые доли
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;

    const timeString = `${this.pad(hours)}:${this.pad(minutes)}:${this.pad(seconds)}.${this.pad(milliseconds)}`;
    document.getElementById('display').textContent = timeString;
  }

  pad(num) {
    return num.toString().padStart(2, '0');
  }
}

Этот вариант точнее, потому что использует реальное время браузера.

React компонент секундомера

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

function Stopwatch() {
  const [time, setTime] = useState(0); // миллисекунды
  const [isRunning, setIsRunning] = useState(false);
  const intervalRef = useRef(null);

  const start = () => {
    if (isRunning) return;
    setIsRunning(true);

    const startTime = Date.now() - time;

    intervalRef.current = setInterval(() => {
      setTime(Date.now() - startTime);
    }, 10); // обновляем каждые 10ms для плавности
  };

  const stop = () => {
    setIsRunning(false);
    clearInterval(intervalRef.current);
  };

  const reset = () => {
    setTime(0);
    setIsRunning(false);
    clearInterval(intervalRef.current);
  };

  const formatTime = (ms) => {
    const totalSeconds = Math.floor(ms / 1000);
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;
    const milliseconds = Math.floor((ms % 1000) / 10);

    const pad = (num) => num.toString().padStart(2, '0');
    return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}.${pad(milliseconds)}`;
  };

  return (
    <div className="stopwatch">
      <div className="display">{formatTime(time)}</div>
      <button onClick={start} disabled={isRunning}>
        Start
      </button>
      <button onClick={stop} disabled={!isRunning}>
        Stop
      </button>
      <button onClick={reset}>
        Reset
      </button>
    </div>
  );
}

export default Stopwatch;

С lap-функцией (как в Apple Watch)

class StopwatchWithLaps {
  constructor() {
    this.elapsedTime = 0;
    this.laps = [];
    this.lastLapTime = 0;
    this.isRunning = false;
    this.animationId = null;
  }

  start() {
    if (this.isRunning) return;
    this.isRunning = true;

    const startTime = performance.now();
    let lastFrameTime = startTime;

    const update = (currentTime) => {
      const deltaTime = currentTime - lastFrameTime;
      this.elapsedTime += deltaTime;
      lastFrameTime = currentTime;

      this.display();

      if (this.isRunning) {
        this.animationId = requestAnimationFrame(update);
      }
    };

    this.animationId = requestAnimationFrame(update);
  }

  stop() {
    this.isRunning = false;
    if (this.animationId) cancelAnimationFrame(this.animationId);
  }

  lap() {
    if (!this.isRunning) return;

    const lapTime = this.elapsedTime - this.lastLapTime;
    this.laps.push({
      lapNumber: this.laps.length + 1,
      time: this.formatTime(lapTime),
      totalTime: this.formatTime(this.elapsedTime)
    });
    this.lastLapTime = this.elapsedTime;
    this.updateLapsList();
  }

  reset() {
    this.elapsedTime = 0;
    this.laps = [];
    this.lastLapTime = 0;
    this.stop();
    this.display();
    this.updateLapsList();
  }

  display() {
    const timeString = this.formatTime(this.elapsedTime);
    document.getElementById('display').textContent = timeString;
  }

  updateLapsList() {
    const lapsList = document.getElementById('laps');
    lapsList.innerHTML = this.laps
      .map(
        (lap) =>
          `<div class="lap">
            <span>Lap ${lap.lapNumber}: ${lap.time}</span>
            <span>${lap.totalTime}</span>
          </div>`
      )
      .join('');
  }

  formatTime(ms) {
    const totalSeconds = Math.floor(ms / 1000);
    const minutes = Math.floor(totalSeconds / 60);
    const seconds = totalSeconds % 60;
    const milliseconds = Math.floor((ms % 1000) / 10);

    const pad = (num) => num.toString().padStart(2, '0');
    return `${pad(minutes)}:${pad(seconds)}.${pad(milliseconds)}`;
  }
}

Пример HTML

<div class="stopwatch-container">
  <div class="display" id="display">00:00:00.00</div>
  <div class="controls">
    <button id="start" class="btn btn-primary">Start</button>
    <button id="stop" class="btn btn-secondary" disabled>Stop</button>
    <button id="lap" class="btn btn-tertiary" disabled>Lap</button>
    <button id="reset" class="btn btn-danger">Reset</button>
  </div>
  <div class="laps" id="laps"></div>
</div>

CSS для красивого отображения

.stopwatch-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  padding: 32px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 16px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
  max-width: 400px;
}

.display {
  font-size: 48px;
  font-weight: bold;
  font-family: 'Monaco', monospace;
  color: white;
  letter-spacing: 4px;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}

.controls {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  width: 100%;
}

.btn {
  padding: 12px 24px;
  font-size: 16px;
  font-weight: 600;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.2s ease;
}

.btn-primary {
  background: #4CAF50;
  color: white;
}

.btn-primary:hover:not(:disabled) {
  background: #45a049;
  transform: translateY(-2px);
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.laps {
  width: 100%;
  max-height: 200px;
  overflow-y: auto;
  background: rgba(0, 0, 0, 0.1);
  border-radius: 8px;
  padding: 12px;
}

.lap {
  display: flex;
  justify-content: space-between;
  padding: 8px;
  color: white;
  border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}

Важные моменты

  1. Используй performance.now() — точнее чем Date.now()
  2. Используй requestAnimationFrame — синхронизирует с монитором (60fps)
  3. Отключай интервалы — clearInterval, чтобы не было утечек памяти
  4. Обновляй каждые 10-50ms — для плавной визуализации
  5. Сохраняй в localStorage — чтобы секундомер работал при refresh
  6. Обработай pause/resume — не всегда reset после паузы

Bonus: сохранение в localStorage

save() {
  localStorage.setItem('stopwatch', JSON.stringify({
    elapsedTime: this.elapsedTime,
    isRunning: this.isRunning,
    timestamp: Date.now()
  }));
}

restore() {
  const saved = localStorage.getItem('stopwatch');
  if (saved) {
    const { elapsedTime, isRunning, timestamp } = JSON.parse(saved);
    const timePassedSinceSave = Date.now() - timestamp;

    this.elapsedTime = elapsedTime + (isRunning ? timePassedSinceSave : 0);
    if (isRunning) this.start();
  }
}

Так секундомер будет работать даже после перезагрузки страницы!

Как реализовать секундомер? | PrepBro