JavaScriptを使用してストップウォッチを作成する。具体的な仕様としては、
1 最初に画面に表示した時にはディスプレイに0が表示される。
2 スタートボタンを押すと、1秒ごとにディスプレイの表示が0,1,2・・・(開始からの秒数)と増えていく(カウントアップする、という)。
3 カウントアップ状態のときにストップボタンを押すと、ディスプレイの表示がその秒数で停止する。
4 再度スタートボタンを押すと、ディスプレイの表示が改めて0に戻り、0,1,2・・・と増えるカウントアップ状態に戻る。
5 カウントアップ状態になったときに、その時刻と、「開始」という表示がストップウォッチの下に操作ログとして追記される。
6 停止状態になったときに、その時刻を「終了」という表示がストップウォッチの下に操作ログとして追記される。
ボタンを表示し、JavaScriptが動く準備をする
<!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>
//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秒ごとに呼び出されるようにする。②
//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・・・と表示されていくことが確認できる。
動作としては①で定義したsecondsの値が、1秒ごとに6行目の++で1ずつ加算されている。
setIntervalでの第一引数、第二引数とは?
setIntarvalは上記のように第一引数と第二引数が必要になるが、最初学習したときは、どれが第一引数で、どれが第二引数かわからなかった。そこで、もっと簡便化してかくと、こうなる。 setInterval(第一引数,第二引数) なので、「,」で区切られている部分を探せば、どこが第二引数にあたるのかがわかる。今回でいえば、1000が第二引数である。ということは、 function(){ seconds++; console.log(seconds); } ここまでが第一引数ということになる。このfunctionの意味はすでに書いたので割愛。//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・・・と順番に増える動きが実現できた。停止ボタンが機能していないので、画面をリロードするしかないが、動きとしては想定したものに近くなってきた。
見た目を整える
動きが整ってきたので、cssで装飾する。
<!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>
.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;
}
段々形になってきた。
カウント停止を実現し、バグに対応する
ストップボタンが押されたらイベントを停止する
ストップボタンの動作では、スタートボタンで動かし始めたsetIntervalを停止させることが必要である。その方法もブラウザから提供されている。
まず、setIntervalは呼び出したときに「開始した繰り返し処理を識別する数値」を返す(intervalIDという)。
「開始した繰り返し処理」について
今回でいうと、「開始した繰り返し処理」とは、
function(){
seconds++;
displayElm.innerText = seconds;③
console.log(seconds);
}
のことである。つまり、「displayというクラスがついた一番最初の文字(つまり0)」を、secondsを足していってinnerTextに反映させてね。」という**関数を繰り返し処理しているのである。**ここが非常に理解が難しかったところで、「繰り返し処理」が2つあるところで混乱した。
1つ目の繰り返し処理が、先ほどの関数内の繰り返し処理。2つ目が、関数そのものを一定の時間間隔で繰り返すというもの。
さて、これらをまとめると、「setIntervalは、開始した繰り返し処理 = functionそのものを繰り返すということになる。
let intervalID = setInterval([一定時間で動かしたい処理の入っている関数],[動作の間隔])
そして、intervalIDを指定してclearntervalを呼び出すとその繰り返し処理を停止することができる。
clearInterval(intervalID)
setInterval、clearIntervalを組み合わせてコードを書いていく。setntervalの数値を入れておくための変数としてtimerという変数を準備する。①
setntervalの戻り値をtimerに代入しておくことで、intervalIDを持ち続けることができる。
//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を代入する。
デバッグ(debug)する
ストップウォッチがだいぶ完成してきたところで、以下のような状況に出くわした。
・まれにストップを押してもなぜかタイマーが止まらない
・まれにカウントアップが1秒以下のタイミングで起きてしまう
・どちらもリロードするとなおる
これらは想定していない動きなので、バグ(bug)と呼ばれる。このようなバグを突き止めて治すことをデバッグ(debug)という。
ボタンとタイマーの制御に関連するところが原因だと思われるので、①の「新しくタイマーが開始されたところ(関数の終了直前)」でログを出し、②の「これからタイマーを止める場所」でもconsole.logをだす。
どちらも timerの値を確認できるようにしている。
//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の結果は以下のようになった。
今回の内容は、「スタートを押して、ストップを押す前にさらにスタートを押す」とバグが起きることがわかった。
プログラムの中の動きで表現すると、「タイマーが動いている最中に、さらにタイマーを起動した」ときにバグが起きていると解釈できる。タイマーの動作はtimer変数一つで管理しているので、連続でスタートボタンを押すと前のタイマーのintervalIDが上書きされて、先に動かしたタイマーを止められないということがわかる。
これは当初考えていなかった仕様なので、次のようにする。
・タイマーが動いている時にスタートボタンを押しても何もしない
・タイマーが停まっている時にストップボタンを押しても何もしない
先の表にボタンの動作も付け加えると次のようになる。
アプリケーションの状態 | timer変数の値 | スタートボタンの動作 | ストップボタンの動作 |
---|---|---|---|
初期状態 | null | カウントアップ開始 | 何もしない |
カウントアップ状態 | intervalIDとして扱われる何らかの数値 | 何もしない | 停止する |
停止状態 | null | カウントアップ開始 | 何もしない |
これをコードの言葉で表現すると、
・もしもtimer変数がnullならば、startButtonのクリックが動作する
・もしもtimer変数がnullでなければ、 stopButtonのクリックが動作する
と書き換えられる。つまりif文を使うだけで実現できそうである。
//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;
}
});
演算子が「===」というのは、厳密等価演算子という演算子で、型変換することなく厳密に等価比較を行うことができる。
これで先ほどのバグが修正された。
次回に続く。