Back to Top
Room A
WEB - JavaScript
  • Twitter
  • Facebook
  • Pinterest
  • Hatena
  • instagram
  • YouTube

JavaScript : 複数のタイマー(カウントダウン)処理を同時に実行【setInterval】

2021.03.01

JavaScript の setInterval関数を使ったカウントダウン(タイマー)ですが、1ページ内に配置された複数のタイマー処理を同時に実行するコードを書いてみました。

1ページ内で1つのタイマー処理のみを行う場合は getElementById でページ内の一意の場所を特定、設定した間隔(多くは1秒ピッチ)でカウントダウン表示を実行するのが主流(?)のようですが、例えば amazonのタイムセールのページのように同一ページ内に複数のカウントダウンタイマーが配置され、それらを同時に処理する必要がある場合では getElementById ではなく、getElementsByClassName メソッドなどで処理対象の要素を特定(限定)する必要が出てきます。

ぶっちゃけ、このサンプルでは getElementById の部分を getElementsByClassName にしただけ...であり、現在の自分のスキルではこれが精一杯であります。

また、setInterval とセットで語られる事の多い clearInterval の使い方を誤ると1ヶ所のみのタイマー処理では発生しないエラーが出て『???』という事になってしまうので(※詳細後述)、その辺りの備忘録をも兼ねています。(要は、自分はバリバリの初心者であります...)

処理の流れ

JavaScriptによる1秒間隔での処理は以下のようになります。(※PHP部分は後述)

  • 同一クラス名のdiv要素( end_time と time_left )を getElementsByClassName で取得
  • 取得した終了時刻と現在時刻のUNIXタイムスタンプの差分のみを新たな配列に格納
  • 配列内の要素数を元に for文でループ処理
  • 計算した時間差によって処理(表示)を分岐
  • 時間差を残りの時間表示に置換
  • 残り時間をページに表示
  • 全ての残り時間がゼロになったら clearInterval でループ処理終了

このサンプルでは設定時刻を PHP を使って動的に設定していますが、実際にはデータベースから設定時刻を読み出し・表示したり、JavaScriptで時間を設定する事が多いと思います。しかし、このサンプルのような getElementsByClassName の使い方をするのであれば、『値(end_time)』と『表示(time_left)』部分には必ず同一クラス名を命名するのが基本となります。

また、このサンプルでは残り時間やタイムアップをページに表示する際に、文字の色を書き換えるようにしています。

動作確認 & コードサンプル



cdt_100_sample.php : CODE


<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>同一ページ内 複数同時のカウントダウン処理:サンプル</title>
<style>
.end_time {
font-size: 1.6em;
font-weight: bold;
}
.time_left {
font-size: 1.2em;
font-weight: bold;
}
</style>
</head>

<body>
<h1>JavaScript : setInterval</h1>
<h2>Countdown Timer : SAMPLE</h2>
<hr>

<?php
$loop = 6;      //ループ回数
$plus = 5;      //加算秒数:5秒
$now = time();  //現在時刻のUnixタイムスタンプ

for ( $i=1; $i<=$loop; $i++ ){
	$j = mt_rand(1,6);                        //1~6の範囲で乱数を発生
	$set = $now + ($j * $plus);               //現在のUNIXタイムに秒数(最大:30秒)を加算
	$limit_date = date('Y-m-d H:i:s',$set);   //年月日・時分秒に置換

	echo "<div class=\"end_time\">設定時間 : ".$limit_date."</div>\n";
	echo "<div class=\"time_left\"></div>\n";
	echo "<hr>";
}
?>

<script>
function CountDownTimer(){

	const ele_end = document.getElementsByClassName('end_time');    //クラス名:end_time 要素取得
	const ele_lim = document.getElementsByClassName('time_left');   //クラス名:time_left 要素取得

	if( typeof ele_end[0] != 'undefined' ){

		const arr_cnt = new Array();             //時差格納配列
		const now_time = new Date;                   //現在時刻
		const now_unix = Date.parse ( now_time );    //UNIX time stamp

		for (var i=0; i < ele_end.length; i++ ){
			const end_time = ele_end[i].innerHTML;          //タイマー設定時刻
			const end_unix = Date.parse ( end_time );       //UNIX time stamp
			arr_cnt[i] = ( end_unix - now_unix ) / 1000;   //Time interval
		}

var ci = 0;  //カウント確認用
		var id = setInterval(function(){
ci++;
		for (var i=0; i < ele_end.length; i++ ){
			var cnt = arr_cnt[i];   //チェック対象時差

			if( cnt > 0 ){
				cnt--;              //1秒減算
				arr_cnt[i] = cnt;   //減算した値を再代入

				if( cnt > 86400 ){   //Over:1day
					var vd = Math.floor( cnt / (24 * 60 * 60) );
					var vH = Math.floor( cnt % (24 * 60 * 60) / (60 * 60) );
					var vi = Math.floor( cnt % (24 * 60 * 60) % (60 * 60) / 60 );
					var vs = cnt % (24 * 60 * 60) % (60 * 60) % 60;
					var cnt_message = '残り時間:' + vd + '日' + vH + '時間' + vi + '分' + vs + '秒';
				}

				if( cnt < 86400 ){   //Under:1day
					var vH = Math.floor( cnt % (24 * 60 * 60) / (60 * 60) );
					var vi = Math.floor( cnt % (24 * 60 * 60) % (60 * 60) / 60 );
					var vs = cnt % (24 * 60 * 60) % (60 * 60) % 60;
					var cnt_message = '残り時間:' + vH + '時間' + vi + '分' + vs + '秒';
				}
				document.getElementsByClassName('time_left')[i].style = "color:blue;";
			}

			if( cnt == 0 ){
				var cnt_message = 'Time is UP!';
				document.getElementsByClassName('time_left')[i].style = "color:red;";
			}
			document.getElementsByClassName('time_left')[i].innerHTML = cnt_message;
		}

		if( arr_cnt.every( val => val == 0) ){
			clearInterval(id);
		}

console.log(ci);
console.log(arr_cnt);

		},1000);
	}
}

window.onload = function(){
	CountDownTimer();
}
</script>

</body>
</html>

解説・注意点

PHPの処理


<?php
$loop = 6;      //ループ回数
$plus = 5;      //加算秒数:5秒
$now = time();  //現在時刻のUnixタイムスタンプ

for ( $i=1; $i<=$loop; $i++ ){
	$j = mt_rand(1,6);                        //1~6の範囲で乱数を発生
	$set = $now + ($j * $plus);               //現在のUNIXタイムに秒数(最大:30秒)を加算
	$limit_date = date('Y-m-d H:i:s',$set);   //年月日・時分秒に置換

	echo "<div class=\"end_time\">設定時間 : ".$limit_date."</div>\n";
	echo "<div class=\"time_left\"></div>\n";
	echo "<hr>";
}
?>



PHPにて【end_time】と【time_left】のクラス名を持つ <div> ダグを6組のペアで生成しています。

PHP の mt_rand()関数を使って1~6までの乱数を発生させているのは、実際のWEBページでは必ず残り時間の多い順に処理対象(この場合は end_time)が並ぶとは限らないので、敢えてランダムな時間差が発生するようにしています。逆に処理対象が正確に昇り順・降り順となっていても問題ありません。

各終了時刻は現在の時刻、正確にはブラウジングを行っている端末上(PCやスマホ)のシステム時刻を time()関数にてUNIXタイムスタンプで取得し、各 end_time には現在時刻に先の乱数に5(秒)を掛けた数値(秒数)を足した時刻がランダムに設定されるようにしています。

但し、mt_rand()関数は、必ずしもユニークな値を発生するとは限らないので6ヶ所の end_time要素には重複した同じ終了時刻が設定される場合もあります。※逆に同時刻であっても正常に処理されることが確認できます。

よって、現時刻に対して一番近い終了時刻は【+5秒】となり、最大でも【+30秒】の範囲内で6ヶ所の終了時刻が設定されます。

変数:$loop の値や $plus の値を変更する事で箇所数や時間差を変化させることができますが、大きな時間差が発生するような値を指定してしまうと動作確認に時間をとられる事になるので注意してください。(笑)

本番では終了時刻が等間隔の時差とならず不規則な時間差になると思いますが、時間差や並び順が影響を受けないように JavaScript を書いたつもりです。但し、このスクリプトの仕様として終了時刻表示(end_time)とカウントダウン表示される(time_left)は、必ず同数(ペア)でないと正常に動作しません。

PHP.net 公式マニュアル

  • time - 現在の Unix タイムスタンプを返す
  • date - ローカルの日付/時刻を書式化する
  • mt_rand - メルセンヌ・ツイスター乱数生成器を介して乱数値を生成する


getElementsByClassName 処理


<script>
function CountDownTimer(){

	const ele_end = document.getElementsByClassName('end_time');    //クラス名:end_time 要素取得
	const ele_lim = document.getElementsByClassName('time_left');   //クラス名:time_left 要素取得

	if( typeof ele_end[0] != 'undefined' ){

ユーザー関数名を CountDownTimer に設定。

WEBページ内でスクリプトを <script src = " "> の形式で配置(宣言)しやすくするために function CountDownTimer() とし、window.onload で呼び出していますが、window.onload を使いたくない、もしくは不要の場合には function CountDownTimer() の中身だけを使ってください。

getElementsByClassName メソッドでクラス名【end_time】と【time_left】のdiv要素、及びPHPで生成した設定時刻をページ全体から取得し、変数:ele_end と変数:ele_lim に各々を格納しています。

念の為、ele_end[0] をチェックして 'undefined' でなければ処理を実行するように条件分岐させていますが、getElementsByClassName のターゲットとなるクラス名を持つ有効なHTML要素がページ内に100%存在するならばこの条件分岐は不要かと思います。

処理結果が表示されるdiv要素【time_left】は空の要素としています。予め文字列が記述されていてもスクリプトによって強制的に書き換えられてしまいます。

前述の通り【end_time】と【time_left】は必ず一対となっている必要があり、もしペアになっていない場合は互いの配列の順序にズレが生じ、最終処理結果(残り時間)が意図しない位置に表示されることになります。



処理対象の抽出(新たな配列の生成)


		const arr_cnt = new Array();             //時差格納配列
		const now_time = new Date;                   //現在時刻
		const now_unix = Date.parse ( now_time );    //UNIX time stamp

		for (var i=0; i < ele_end.length; i++ ){
			const end_time = ele_end[i].innerHTML;          //タイマー設定時刻
			const end_unix = Date.parse ( end_time );       //UNIX time stamp
			arr_cnt[i] = ( end_unix - now_unix ) / 1000;   //Time interval
		}

実は、このスクリプトを書く時に一番悩んだのは setInterval のループ処理ではなく、処理対象となるデータ(時間差)の前処理でした(笑)

一番最初に実行する getElementsByClassName の処理を setInterval のループ処理内に含めてしまうと、1秒毎に getElementsByClassName もその都度実行されてしまうのですが、それでは無駄にリソースを消費してしまいますし、数が多くなるとパフォーマンスにも影響が出てしまいます。

...で、プログラムが開始された時の開始時刻やページ内に記述(設定)された終了時刻は常に一定であり、プログラムが開始された時点でのタイムアップまでの残り時間(差分)は setInterval のループ処理を通さない限り変化しない点に着目し、ならば開始直後の時間差だけを setInterval の前段階で計算して配列にセット、その配列を setInterval のループ内で処理させるようにすればイイんじゃね?...と

その時差のみの値を入れるための空の配列 arr_cnt を用意して、あとは現在の時刻やUNIXタイムスタンプ、ページ内に記述された終了時刻を getElementsByClassName で取得し、各々の差分(UNIXタイムスタンプ)を計算、さらにミリ秒単位を1/1秒に置換して、最後に配列のキーを基準にして取得した順番に arr_cnt に値を代入しています。


MEMO:

このスクリプトではWEBページが表示された時点で、残り時間がマイナス1秒された表示になりますが、開始時点で『XX時 XX分 00秒』としたい場合は arr_cnt[i] = ( end_unix - now_unix ) / 1000; を...


			arr_cnt[i] = ( end_unix - now_unix + 1000 ) / 1000;

...として、予め時差に1秒プラスしてやれば、00秒(このスクリプトでは00秒かn5秒)表示でカウントダウンがスタートします。



setInterval 処理


		var id = setInterval(function(){   //Count Down Timer : 1000 mil/second

			~ ココに処理を記述 ~

		},1000);   //1000ミリ秒=1秒

この部分がこのスクリプトの『肝』でありますが、記述された命令が1000ミリ秒=1秒間隔で実行されます。

この setInterval の書式も...


var id = setInterval("CountDownTimer()",1000); 
    
function CountDownTimer(){

     ~ ココに処理を記述 ~
}

...上記のように記述することができますが、後者の書式は一見するとスッキリとして見通しの良い書式に見えても、1回だけ処理すれば良い部分を function CountDownTimer() 内に記述してしまうと、それらが無駄に1秒毎に実行される事になるので非効率なスクリプトとなってしまいます。なので、どちらの書式にするかは、実行する処理内容を精査して選択する必要があります...。(※経験談)

ちなみに...


var ci = 0;  //カウント確認用
		var id = setInterval(function(){
ci++;

の var ci = 0; と ci++; は、動作確認用のカウンタなので削除しても問題ありません。



時間(秒数)の変換/差分処理


		for (var i=0; i < ele_end.length; i++ ){

			var cnt = arr_cnt[i];   //チェック対象時差

			if( cnt > 0 ){

				cnt--;              //1秒減算
				arr_cnt[i] = cnt;   //減算した値を再代入

ele_end.length で取得した値をカウンタにセットした for文にて、処理対象となる一意の値を arr_cnt[i] から抽出し、値がゼロでなければ1秒減算。そして減算後の値を再度 arr_cnt[i] に戻しています。(こうしないと残り時間が固定されたまま変化しません)



1日以上/1日以下の処理分岐


				if( cnt > 86400 ){   //Over:1day
					var vd = Math.floor( cnt / (24 * 60 * 60) );
					var vH = Math.floor( cnt % (24 * 60 * 60) / (60 * 60) );
					var vi = Math.floor( cnt % (24 * 60 * 60) % (60 * 60) / 60 );
					var vs = cnt % (24 * 60 * 60) % (60 * 60) % 60;
					var cnt_message = '残り時間:' + vd + '日' + vH + '時間' + vi + '分' + vs + '秒';
				}

				if( cnt < 86400 ){   //Under:1day
					var vH = Math.floor( cnt % (24 * 60 * 60) / (60 * 60) );
					var vi = Math.floor( cnt % (24 * 60 * 60) % (60 * 60) / 60 );
					var vs = cnt % (24 * 60 * 60) % (60 * 60) % 60;
					var cnt_message = '残り時間:' + vH + '時間' + vi + '分' + vs + '秒';
				}
				document.getElementsByClassName('time_left')[i].style = "color:blue;";

どれぐらいの時間差となるかはWEBページによって異なると思いますが、ココでは残り時間が1日以上と以下で表示内容を変えて、残り時間の表示は 変数:cnt_message に代入します。※年-月-日 時・分・秒の変数表記は、PHPの Y-m-d H:i:s の表記に合わせています。

1日(24時間)は 86400秒なので cnt の値がソレ以上か以下の条件で処理を振り分けています。場合によっては『年単位』や『月単位』での表示も必要になるかもしれませんが、必要に応じてアレンジしてください。

(24 * 60 * 60) や (60 * 60) の秒数計算部分は、 (24 * 60 * 60) ならば (86400)、 (60 * 60) ならば (3600) のように、計算式ではなく直接数値を入れた方が処理速度が上がると思います。

このサンプルでは6ヶ所(6組)の処理をするだけなので問題はないのですが、対象が多くなる場合は、例え単純な乗算だとしても、少しでも負荷を減らすような書き方をした方が良いと思います。

最後に、残り時間のカウント中(表示中)は青色で表示されるようにしています。



タイムアップ時の処理


			if( cnt == 0 ){
				var cnt_message = 'Time is UP!';
				document.getElementsByClassName('time_left')[i].style = "color:red;";
			}

変数: cnt の値がゼロとなった時点で『Time is UP!』の文字列を 変数:cnt_message に代入します。カウント終了時はカウント中の青色表示に対して赤色表示されるようにしました。



最終出力


			document.getElementsByClassName('time_left')[i].innerHTML = cnt_message;

最後に 変数:cnt_message 内に代入された文字列がブラウザに出力されます。



clearInterval 処理


		if( arr_cnt.every( val => val == 0) ){
			clearInterval(id);
		}

処理対象データと同様に悩んだのが clearInterval の処理です。

最初は短絡的に...


			if( cnt == 0 ){
				clearInterval(id);
				var cnt_message = 'Time is UP!';
				document.getElementsByClassName('time_left')[i].style = "color:red;";
			}

...として、変数: cnt の値がゼロとなった時点で clearInterval を宣言してしまっていたのですが、コレでは6ヶ所のウチのひとつでもタイムアップすると、その時点ですべての setInterval のループ処理が終了してしまうことになります。

そこで、これはデータの前処理とも大きく関係するのですが、every() で arr_cnt 配列の値をチェックし、arr_cnt 配列内のすべての値がゼロとなったら clearInterval が実行されるようにしました。

clearInterval で処理を終了しなくてもブラウザを閉じればそれで終わり...なのですが、タイムアップした後でもスクリプトが動作し続けてリソースを消費するのはマズいので、お行儀良く終わるように...( ˘ω˘)ウンウン



おまけ


console.log(ci);
console.log(arr_cnt);

コンソールログを確認すると処理が進む(時間が進む)ごとに処理回数を示す ci と 配列:arr_cnt 内の値が変化し、最後には配列内の全ての値がゼロになり ci のカウントも停止(すなわち setInterval が終了)する事が確認できます。

コンソールログ/開始時
コンソールログ/終了時


最後に...

JavaScript の配列処理(関数)に馴れておらず、ついPHPのような処理の仕方をしてしまうのですが、今回、色々と調べる過程でJavaScriptにも便利な配列関数が存在する事を知りました。

取り敢えず、自分の目的に合致したスクリプトは組めた...と思いますが、処理対象の箇所数が100ヶ所とか1000ヶ所とかになった場合に、ループ処理にかかる時間がそのままカウントダウンの誤差になる気がするのですが、その辺りはどうなんでしょうか? まぁ、それだけの処理するとなるとループ手法はNGなんでしょうね...

恐らく同じ処理をするにしても、もっとシンプルでスマートな書き方ができると思うのですが、JavaScript の foreach の攻略(?)も含めて、ソレらは今後の課題としたいと思います。