LoginSignup
0
0

More than 1 year has passed since last update.

JavaScriptを使ってストップウォッチを作成する 前半

Last updated at Posted at 2022-12-14

 JavaScriptを使用してストップウォッチを作成する。具体的な仕様としては、
1 最初に画面に表示した時にはディスプレイに0が表示される。
2 スタートボタンを押すと、1秒ごとにディスプレイの表示が0,1,2・・・(開始からの秒数)と増えていく(カウントアップする、という)。
3 カウントアップ状態のときにストップボタンを押すと、ディスプレイの表示がその秒数で停止する。
4 再度スタートボタンを押すと、ディスプレイの表示が改めて0に戻り、0,1,2・・・と増えるカウントアップ状態に戻る。
5 カウントアップ状態になったときに、その時刻と、「開始」という表示がストップウォッチの下に操作ログとして追記される。
6 停止状態になったときに、その時刻を「終了」という表示がストップウォッチの下に操作ログとして追記される。

ボタンを表示し、JavaScriptが動く準備をする

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>StopWatch</title>
</head>
<body>
  <button class = "startButton">スタート</button>
  <button class = "stopButton">ストップ</button>
  <script src = "./main.js"></script>
</body>
</html>
main.js
//startButtonというclassがついているタグ要素のうち、
//最初のもの(スタートボタン)を取り出す
let startButton = document.getElementsByClassName("startButton")[0];

//取り出したstartButtonに対してクリックイベントのリスナを仕掛ける
startButton.addEventListener("click",function(){
  //この行はクリックしたときよばれる
  console.log("start")  
});

①はstartButtonというclassが指定されている要素のうち最初のものを取り出している。
流れとしては以下のようなものである。
1 document.getElementsByClassName("startButton")で、HTMLのドキュメントから、startButtonというクラスがついている要素を全て取得できる。
2 取得してきた値は配列と同じように操作できるので、document.getElemenetsByClassName("startButton")[0]で、一番初めに画面に出てきた要素を取り出した。
3 この要素は画面上でスタートボタンと呼ばれているボタンである。

startButton.addEventListnerでは、startButtonがクリックされたときのイベントリスナを仕掛けている。この箇所は対象のイベントが発生した時に動き出す仕組み(コールバック)になっている。

経過時刻をカウントし、見た目を整える

スタートボタンが押されたら一定時間ごとにイベントを呼び出す

スタートボタンが押された時の動作を作成する。ここでは一定時間ごとに処理を繰り返すことができるsetIntervalを使う。setIntervalは第一引数の関数の内容を、第二引数の時間の間隔で何度も呼び出す機能がある。第二引数はミリ秒単位なので、ここでは1000を指定して、1秒ごとに呼び出されるようにする。②

main.js
//startButtonというclassがついているタグ要素のうち、
//最初のもの(スタートボタン)を取り出す
let startButton = document.getElementsByClassName("startButton")[0];

//取り出したstartButtonに対してクリックイベントのリスナを仕掛ける
startButton.addEventListener("click",function(){
  //この行はクリックしたときよばれる
  console.log("start");
  let seconds = 0;
  setInterval(function(){
    seconds++;
    console.log(seconds);
  },1000); 
});

画面を更新してスタートボタンを押すと、コンソールにstartと表示されたあと、1,2,3・・・と表示されていくことが確認できる。
8a3df99065b1f3fda6cde1ae576c6d96.gif
動作としては①で定義したsecondsの値が、1秒ごとに6行目の++で1ずつ加算されている。

setIntervalでの第一引数、第二引数とは? setIntarvalは上記のように第一引数と第二引数が必要になるが、最初学習したときは、どれが第一引数で、どれが第二引数かわからなかった。そこで、もっと簡便化してかくと、こうなる。 setInterval(第一引数,第二引数) なので、「,」で区切られている部分を探せば、どこが第二引数にあたるのかがわかる。今回でいえば、1000が第二引数である。ということは、 function(){ seconds++; console.log(seconds); } ここまでが第一引数ということになる。このfunctionの意味はすでに書いたので割愛。
### 数えた秒数を画面に表示する 今までは表示をコンソール上のみにしていたので、ブラウザ上にも秒数が表示されるようにする。 ```html:index.html StopWatch
0
① スタート ストップ ``` スタートボタンの上にdivタグを入れて、classをdisplayとした。① 初期値として0を表示したいので、0を入れている。 以下のJavaScriptのコードでは、この新しく追加した要素をdocument.getElementsByClassNameでdisplayElmとして取得する。② setIntervalの中で、displayElmのinnerTextを呼び出すことで、タグに囲まれている文字列(htmlの①の部分)を秒数の数字に変更している。
main.js
//startButtonというclassがついているタグ要素のうち、
//最初のもの(スタートボタン)を取り出す
let displayElm = document.getElementsByClassName("display")[0];
let startButton = document.getElementsByClassName("startButton")[0];

//取り出したstartButtonに対してクリックイベントのリスナを仕掛ける
startButton.addEventListener("click",function(){
  //この行はクリックしたときよばれる
  console.log("start");
  let seconds = 0;
  setInterval(function(){
    seconds++;
    displayElm.innerText = seconds;
    console.log(seconds);
  },1000); 
});

その結果、スタートしてからの秒数が表示されることを確認できた。クラスdisplayのついた要素に囲まれた値が0,1,2・・・と順番に増える動きが実現できた。停止ボタンが機能していないので、画面をリロードするしかないが、動きとしては想定したものに近くなってきた。
389cf84e06ac2366cf9e02138021c6a4.gif

見た目を整える

動きが整ってきたので、cssで装飾する。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>StopWatch</title>
  <link href = "./main.css" rel = "stylesheet">
</head>
<body>
  <h1 class = "title">ストップウォッチ</h1>
  <div id = "stopWatchPanel">
   <div class = "display">0</div>
   <div class = "actions">
     <button class = "startButton">スタート</button>
     <button class = "stopButton">ストップ</button>
   </div>
  </div>
  <script src = "./main.js"></script>
</body>
</html>
main.css
.title {
  text-align: center;
}

#stopWatchPanel {
  margin: 0 auto;
  width: 10em;
}

.display {
  font-weight: bold;
  text-align: right;
  height: 1.5em;
  margin-bottom: .2em;
  padding: .5em;
  color: lightblue;
  background-color: black;
}

.actions {
  text-align: center;
  margin-bottom: 1em;
}

.message {
  text-align: center;
  border-bottom: 1px solid lightblue;
}

c0bd4c96a589bf244ba8b46457013dfd.png

段々形になってきた。

カウント停止を実現し、バグに対応する

ストップボタンが押されたらイベントを停止する

ストップボタンの動作では、スタートボタンで動かし始めたsetIntervalを停止させることが必要である。その方法もブラウザから提供されている。
まず、setIntervalは呼び出したときに「開始した繰り返し処理を識別する数値」を返す(intervalIDという)。

「開始した繰り返し処理」について

今回でいうと、「開始した繰り返し処理」とは、

function(){
    seconds++;
    displayElm.innerText = seconds;
    console.log(seconds);
  }

のことである。つまり、「displayというクラスがついた一番最初の文字(つまり0)」を、secondsを足していってinnerTextに反映させてね。」という**関数を繰り返し処理しているのである。**ここが非常に理解が難しかったところで、「繰り返し処理」が2つあるところで混乱した。
1つ目の繰り返し処理が、先ほどの関数内の繰り返し処理。2つ目が、関数そのものを一定の時間間隔で繰り返すというもの。
さて、これらをまとめると、「setIntervalは、開始した繰り返し処理 = functionそのものを繰り返すということになる。

このsetntervalIdを保持しておくことで、後で繰り返し処理を止めることができる。 setntervalにはいくつか書き方があるが、よく使われる形として、次のようなものがある。
let intervalID = setInterval([一定時間で動かしたい処理の入っている関数],[動作の間隔])

そして、intervalIDを指定してclearntervalを呼び出すとその繰り返し処理を停止することができる。

clearInterval(intervalID)

setInterval、clearIntervalを組み合わせてコードを書いていく。setntervalの数値を入れておくための変数としてtimerという変数を準備する。①
setntervalの戻り値をtimerに代入しておくことで、intervalIDを持ち続けることができる。

main.js
//startButtonというclassがついているタグ要素のうち、
//最初のもの(スタートボタン)を取り出す
let displayElm = document.getElementsByClassName("display")[0];
let timer = null;

let startButton = document.getElementsByClassName("startButton")[0];
//取り出したstartButtonに対してクリックイベントのリスナを仕掛ける
startButton.addEventListener("click",function(){
  //この行はクリックしたときよばれる
  console.log("start");
  let seconds = 0;
  timer = setInterval(function(){
    seconds++;
    displayElm.innerText = seconds;
    console.log(seconds);
  },1000); 
});

let stopButton = document.getElementsByClassName("stopButton")[0];
stopButton.addEventListener("click",function(){
clearInterval(timer);
timer = null;
});

timer変数に入っている値は次の通り。

アプリケーションの状態 timer変数の値
初期状態 null
カウントアップ状態 intervalIDとして扱われる何らかの値
停止状態 null

特に停止状態のときにtimerを初期状態のnullに戻しておくと、「timer変数に値が入っているならカウントアップ状態」ということがわかりやすくなる。

次に、ストップボタンを押した時のイベントリスナを実装する。ボタンの要素を取得してくるところや、リスナを仕掛けるところはスタートボタンと同様である。ストップボタンをクリックされたらclearIntervalを呼び出して、timer変数にnullを代入する。
411ac378bec92ec7e7e42271641f9a2d.gif

デバッグ(debug)する

ストップウォッチがだいぶ完成してきたところで、以下のような状況に出くわした。
・まれにストップを押してもなぜかタイマーが止まらない
・まれにカウントアップが1秒以下のタイミングで起きてしまう
・どちらもリロードするとなおる
4b66e26702990b34db0bacd97f9e4745 (1).gif

これらは想定していない動きなので、バグ(bug)と呼ばれる。このようなバグを突き止めて治すことをデバッグ(debug)という。
ボタンとタイマーの制御に関連するところが原因だと思われるので、①の「新しくタイマーが開始されたところ(関数の終了直前)」でログを出し、②の「これからタイマーを止める場所」でもconsole.logをだす。
どちらも timerの値を確認できるようにしている。

main.js
//startButtonというclassがついているタグ要素のうち、
//最初のもの(スタートボタン)を取り出す
let displayElm = document.getElementsByClassName("display")[0];
let timer = null;

let startButton = document.getElementsByClassName("startButton")[0];
//取り出したstartButtonに対してクリックイベントのリスナを仕掛ける
startButton.addEventListener("click",function(){
  //この行はクリックしたときよばれる

  let seconds = 0;
  timer = setInterval(function(){
    seconds++;
    displayElm.innerText = seconds;
    console.log(seconds);
  },1000); 
  console.log("start" + timer);
});

let stopButton = document.getElementsByClassName("stopButton")[0];
stopButton.addEventListener("click",function(){
  console.log("stop" + timer);
clearInterval(timer);
timer = null;
});

)
バグを再現する状態ができたときのconsole.lohの結果は以下のようになった。
32865d454123735245f2a82c706f69d0.png
今回の内容は、「スタートを押して、ストップを押す前にさらにスタートを押す」とバグが起きることがわかった。
プログラムの中の動きで表現すると、「タイマーが動いている最中に、さらにタイマーを起動した」ときにバグが起きていると解釈できる。タイマーの動作はtimer変数一つで管理しているので、連続でスタートボタンを押すと前のタイマーのintervalIDが上書きされて、先に動かしたタイマーを止められないということがわかる。
これは当初考えていなかった仕様なので、次のようにする。

・タイマーが動いている時にスタートボタンを押しても何もしない
・タイマーが停まっている時にストップボタンを押しても何もしない

先の表にボタンの動作も付け加えると次のようになる。

アプリケーションの状態 timer変数の値 スタートボタンの動作 ストップボタンの動作
初期状態 null カウントアップ開始 何もしない
カウントアップ状態 intervalIDとして扱われる何らかの数値 何もしない 停止する
停止状態 null カウントアップ開始 何もしない

これをコードの言葉で表現すると、
・もしもtimer変数がnullならば、startButtonのクリックが動作する
・もしもtimer変数がnullでなければ、 stopButtonのクリックが動作する

と書き換えられる。つまりif文を使うだけで実現できそうである。

main.js
//startButtonというclassがついているタグ要素のうち、
//最初のもの(スタートボタン)を取り出す
let displayElm = document.getElementsByClassName("display")[0];
let timer = null;

let startButton = document.getElementsByClassName("startButton")[0];
//取り出したstartButtonに対してクリックイベントのリスナを仕掛ける
startButton.addEventListener("click",function(){
  //この行はクリックしたときよばれる
  if(timer === null){
  let seconds = 0;
  timer = setInterval(function(){
    seconds++;
    displayElm.innerText = seconds;
    console.log(seconds);
  },1000); 
 }
  console.log("start" + timer);
});

let stopButton = document.getElementsByClassName("stopButton")[0];
stopButton.addEventListener("click",function(){
  if(timer !== null){
  console.log("stop" + timer);
clearInterval(timer);
timer = null;
  }
});

演算子が「===」というのは、厳密等価演算子という演算子で、型変換することなく厳密に等価比較を行うことができる。
これで先ほどのバグが修正された。

次回に続く。

参考文献
ステップアップJavaScript フロントエンド開発の初級から中級へ進むために

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0