JavaScript | 1 日 120 分 × 7 日アプリ学習:タイマー & ストップウォッチ

JavaScript
スポンサーリンク

4日目のゴールと今日やること

4日目のテーマは
「ストップウォッチとカウントダウンの“共通部分”を整理して、きれいな設計にする」
ことです。

ここまでであなたは、

  • ストップウォッチ(経過時間)
  • カウントダウン(残り時間)
  • setInterval と Date.now() を使った時間管理
  • 開始 / 停止 / 一時停止 / 再開 / リセット
  • ミリ秒表示
  • 多重起動防止(状態管理)

を一通り体験しました。

4日目はここから一歩進んで、

  • 共通ロジックを「部品」として切り出す
  • 時間フォーマット関数を共通化する
  • setTimeout を“補助的な演出”に使う
  • 「状態 × 条件分岐」をコードとして整理する

という、“中級者らしい整理の仕方”をやっていきます。


時間フォーマットを「共通関数」にする

なぜ共通化するのか

ストップウォッチでもカウントダウンでも、
「ミリ秒 → 分:秒.ミリ秒」の変換は同じです。

同じロジックをあちこちにコピペすると、

  • 修正が大変
  • どこかだけ仕様がズレる
  • バグの原因になる

ので、「時間フォーマット専用の関数」に切り出します。

共通フォーマット関数

function formatTime(ms) {
  if (ms < 0) ms = 0;

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

  const mm = String(minutes).padStart(2, "0");
  const ss = String(seconds).padStart(2, "0");
  const mmm = String(milliseconds).padStart(3, "0");

  return `${mm}:${ss}.${mmm}`;
}
JavaScript

深掘りポイント

ここでやっていることはシンプルですが、とても重要です。

  • 「時間の見た目」はこの関数だけが知っている
  • 呼び出し側は「ミリ秒を渡して文字列を受け取る」だけ
  • ストップウォッチもカウントダウンも同じ関数を使える

これで、「時間の表示仕様」を一箇所で管理できるようになります。


表示処理も「役割ごと」に分ける

DOM 更新を一箇所にまとめる

時間表示の更新も、関数にしておきます。

const timeDisplay = document.getElementById("time");

function renderTime(ms) {
  timeDisplay.textContent = formatTime(ms);
}
JavaScript

ストップウォッチ側もカウントダウン側も、
「画面に時間を出したいときは renderTime を呼ぶ」
というルールにしておくと、コードがかなり読みやすくなります。

深掘りポイント

ここまでで、

  • 時間の計算(ロジック)
  • 時間のフォーマット(見た目の文字列)
  • DOM への反映(画面更新)

がきれいに分かれました。

この「役割の分離」が、中級以降のコードではとても大事です。


タイマーの「エンジン部分」を抽象化してみる

共通しているのは「一定間隔で関数を呼ぶこと」

ストップウォッチもカウントダウンも、
本質的にはこうです。

  • setInterval で一定間隔ごとに update を呼ぶ
  • update の中で「今の時間」を計算する
  • 計算結果を表示する

この「setInterval の管理」を、
小さな“エンジン”としてまとめてみます。

シンプルなタイマーエンジン

function createTimerEngine(tickIntervalMs, onTick) {
  let timerId = null;
  let running = false;

  function start() {
    if (running) return;
    running = true;
    timerId = setInterval(() => {
      onTick();
    }, tickIntervalMs);
  }

  function stop() {
    if (!running) return;
    clearInterval(timerId);
    timerId = null;
    running = false;
  }

  return { start, stop, isRunning: () => running };
}
JavaScript

このエンジンは、

  • 何ミリ秒ごとに
  • どの関数を呼ぶか

だけを知っていて、
「時間の意味(経過か残りか)」は知りません。

深掘りポイント

この設計にすると、

  • ストップウォッチ用のエンジン
  • カウントダウン用のエンジン

を同じ仕組みで作れます。

「時間の意味」は外側のロジックが決める。
「一定間隔で呼ぶ」はエンジンが担当する。

という分担ができているのがポイントです。


ストップウォッチをエンジンに乗せて書き直す

ストップウォッチ用のロジック

let swStartTime = 0;
let swElapsedBefore = 0;
let swState = "stopped"; // "stopped" | "running" | "paused"

const stopwatchEngine = createTimerEngine(10, () => {
  const now = Date.now();
  const elapsed = swElapsedBefore + (now - swStartTime);
  renderTime(elapsed);
});
JavaScript

開始・停止・リセット

function startStopwatch() {
  if (swState === "running") return;

  if (swState === "stopped") {
    swElapsedBefore = 0;
  }

  swStartTime = Date.now();
  stopwatchEngine.start();
  swState = "running";
}

function pauseStopwatch() {
  if (swState !== "running") return;

  const now = Date.now();
  swElapsedBefore += now - swStartTime;
  stopwatchEngine.stop();
  swState = "paused";
}

function resetStopwatch() {
  stopwatchEngine.stop();
  swStartTime = 0;
  swElapsedBefore = 0;
  swState = "stopped";
  renderTime(0);
}
JavaScript

深掘りポイント

ここで注目してほしいのは、

  • setInterval / clearInterval は createTimerEngine の中だけ
  • ストップウォッチ側は「時間の計算」と「状態管理」だけに集中している

という構造です。

「タイマーの仕組み」と「時間の意味」を分けることで、
コードの見通しが一気によくなります。


カウントダウンも同じエンジンに乗せる

カウントダウン用のロジック

let cdEndTime = 0;
let cdRemainingMs = 0;
let cdState = "stopped"; // "stopped" | "running" | "paused"

const countdownEngine = createTimerEngine(10, () => {
  const now = Date.now();
  cdRemainingMs = cdEndTime - now;

  if (cdRemainingMs <= 0) {
    cdRemainingMs = 0;
    renderTime(cdRemainingMs);
    finishCountdown();
    return;
  }

  renderTime(cdRemainingMs);
});
JavaScript

開始・一時停止・再開・リセット

function startCountdown() {
  if (cdState !== "stopped") return;

  const inputSeconds = Number(secondsInput.value);
  if (Number.isNaN(inputSeconds) || inputSeconds <= 0) {
    showError("1秒以上を入力してください。");
    return;
  }
  clearError();

  cdRemainingMs = inputSeconds * 1000;
  cdEndTime = Date.now() + cdRemainingMs;

  countdownEngine.start();
  cdState = "running";
}

function pauseCountdown() {
  if (cdState !== "running") return;

  const now = Date.now();
  cdRemainingMs = cdEndTime - now;
  if (cdRemainingMs < 0) cdRemainingMs = 0;

  countdownEngine.stop();
  cdState = "paused";
}

function resumeCountdown() {
  if (cdState !== "paused") return;

  cdEndTime = Date.now() + cdRemainingMs;
  countdownEngine.start();
  cdState = "running";
}

function resetCountdown() {
  countdownEngine.stop();
  cdRemainingMs = 0;
  cdEndTime = 0;
  cdState = "stopped";
  renderTime(0);
}

function finishCountdown() {
  countdownEngine.stop();
  cdState = "stopped";
  // ここでアラームやメッセージを出せる
}
JavaScript

深掘りポイント

ストップウォッチとカウントダウンで、

  • 状態の種類
  • 時間の意味(経過 vs 残り)

は違いますが、

  • createTimerEngine を使っている
  • Date.now() で差分を計算している
  • 状態文字列で操作を制御している

という「骨格」は完全に共通です。


setTimeout を“演出”に使ってみる

例:カウントダウン終了時に点滅させる

setTimeout は「単発の遅延」に向いています。

例えば、
カウントダウンが 0 になったときに
表示を数回点滅させる演出を入れてみます。

function blinkDisplay(times) {
  if (times <= 0) return;

  timeDisplay.classList.toggle("blink");

  setTimeout(() => {
    blinkDisplay(times - 1);
  }, 200);
}

function finishCountdown() {
  countdownEngine.stop();
  cdState = "stopped";
  blinkDisplay(6); // 6回点滅
}
JavaScript

CSS 側で blink クラスを定義しておきます。

.blink {
  visibility: hidden;
}

深掘りポイント

ここでのポイントは、

  • setInterval は「ずっと繰り返す」
  • setTimeout は「一回だけ遅らせる」
  • 再帰的に setTimeout を呼ぶことで「回数付きの繰り返し」が作れる

というところです。

時間アプリの“本体ロジック”は setInterval、
“演出や一時的な動き”は setTimeout、
という役割分担ができると、
アプリの表現力がぐっと上がります。


多重起動防止を「エンジン+状態」で二重に守る

エンジン側の防御

createTimerEngine の中で、
running フラグを見て start / stop を制御しています。

function start() {
  if (running) return;
  running = true;
  timerId = setInterval(onTick, tickIntervalMs);
}

function stop() {
  if (!running) return;
  clearInterval(timerId);
  timerId = null;
  running = false;
}
JavaScript

これで「同じエンジンを二重起動」は防げます。

ロジック側の防御

さらに、ストップウォッチやカウントダウン側でも
state を見て操作を制御しています。

function startStopwatch() {
  if (swState === "running") return;
  // …
}
JavaScript

深掘りポイント

多重起動防止を、

  • エンジンレベル(running フラグ)
  • アプリロジックレベル(state 文字列)

の二段構えでやっているのが、
「中級者らしい設計」です。


今日いちばん深く理解してほしいこと

4日目で一番大事なのは、

  • 時間フォーマットを共通化する
  • setInterval の管理を「エンジン」として切り出す
  • ストップウォッチとカウントダウンは“同じ骨格”で書ける
  • setTimeout は「演出」や「一時的な動き」に向いている
  • 多重起動防止は「状態管理 × エンジン」で守る

という、“設計の視点”です。

ここまで来ると、
もう「とりあえず動くコード」ではなく、
“自分で育てていけるタイマーアプリ”になっています。

5日目以降は、
この設計をベースにして、

  • UI の改善
  • 複数タイマーの同時管理
  • プリセット時間(25分タイマーなど)

といった方向に広げていけます。

タイトルとURLをコピーしました