JavaScriptのタイマー処理 setTimeoutとその活用

以前id:amachangさんのブログでJavaScriptのsetTimeoutの活用方法が掲載されていたのですが、当時はいまいち意味が分かりませんでした。その補足というのはあつかましいですが、簡単な解説と応用をのせておきます。なお、setTimeoutだけに限らず、JavaScriptのタイマー処理に関する話になります。

setTimeoutのはなし

setTimeoutは指定した時間後に、指定した関数を実行してくれる関数です。JavaScriptで数秒間処理を待ちたい場合、最も単純なものに次のような方法があります。

for(var i = 0 ; i < 10000 ; i++)
	for(var j = 0 ; j < 10000 ; j++); //何もしない

これは単に処理を重くしているだけで、ほとんどのブラウザでは、このように何回もループが続くとエラーが出ます。これをエラーが出ないようにした処理が、人力検索はてなで紹介されていた以下の方法です。ちなみに2000とは、2000ミリ秒、つまり二秒のことです。

function sleep(time) {
	var d1 = new Date().getTime();
	var d2 = new Date().getTime();
	while (d2 < d1 + time) {
		d2 = new Date().getTime();
	}
	return;
}
sleep(2000);

こちらのほうが、最初にあげたものより優秀ですが、基本的にはこれら二つは同じ働きをします。最初の例はforを二回重ねることによって、プログラムの途中で処理を遅延していました。また、二つ目の例もsleep(2000)として、sleep関数を呼び出すことで、処理を遅延させました。これに対して、今回メインで取り上げるsetTimeoutは少し特殊な処理をします。setTimeoutは指定した時間後に、指定した関数を実行するものです。例をみてみます。

function foo(){
	setTimeout( function() {
		alert("タイマーで実行");
	}, 3000);
	
	alert("先に実行");
}
foo();

これを実行すると、二つ目のalertのほうが先に実行され、foo関数の実行が終わった時点から、3秒後にタイマーで指定した関数が実行されます。正確にいうと、実行するものがなくなった時点から、3秒後に実行されます。例えば次のようなbarという関数を追加して実行してみます。

function foo(){
	setTimeout( function() {
		alert("タイマーで実行");
	}, 3000);
	
	alert("先に実行1");
}
function bar(){
	foo();
	alert("先に実行2");
}
bar();

これを実行すると、

「先に実行1」
「先に実行2」
「タイマーで実行」

という順番で文字が表示されます。この例では、barの実行が終わってから三秒待って、タイマーで指定した関数が実行されています。呼び出しの階層をまとめると以下のようになります。barがfooを呼び、fooの内部でsetTimeoutとalertが実行され、barに戻ったところでalertが実行されます。そしてbarの実行が終わってから、三秒後にsetTimeoutに指定した関数、この場合は無名の関数が呼ばれます。最後にそのなかで、alertを呼んでいます。

-bar
  -foo
    -setTimeout
    -alert
  -alert

(3秒)

-function
  -alert

何がいいの?

setTimeoutは、指定した時間後に指定した関数を呼び出すタイマーの働きをしますが、それとは別の利点があります。setTimeoutで指定した関数は、必ず一連の処理が終わってから実行されることが保障されています。これを使うことによって、呼び出し階層がどんどん深くなるプログラムを防ぐことができます。例えば、上で挙げたのとは別で、とある関数fooと、とある関数barがあるとします。fooとbarは、ある条件に応じて、お互いにお互いを呼び出すとします。

function foo(){

	…

	bar();

}

function bar(){

	…

	foo();
}

すると以下のように呼び出し階層がどんどん深くなってしまいます。

-bar
  -foo
	-bar
	  -foo
		-bar
		  -foo
			-bar
			  -foo
				-bar
				  -foo

こんな風にどんどん階層が深くなっていくと、一番最初のbarが、いつ終了するのか分かりにくくなります。そこで、setTimeoutを活用します。

function foo(){

	…

	setTimeout( bar , 10);

}

function bar(){

	…

	setTimeout( foo , 10);

}

fooとbarが、setTimerで互いを指定していることが分かります。ここでのポイントはタイマーの時間です。タイマーの時間はミリセカンドで指定するので10/1000秒、つまり0.01秒です。タイマーとしての役割はほとんどありません。しかしこれを使うと、以下のように呼び出し階層がすっきりします。これでプログラマーが処理の流れを理解しやすくなりました。

-bar

(0.01秒後)

-foo

(0.01秒後)

-bar

(0.01秒後)

-foo

さらに活用

先ほど挙げたのは、関数の呼び出し階層をすっきりさせるためでしたが、次はこれを本来の用途であるタイマーとして使ってみます。この方法を使うと、一つの関数内でタイマーの処理を完結させることができます。

var timer = false;

function foo(){
	if(timer == false){
		setTimeout(function() {
			timer = true;
			foo();
			timer=false;
		}, 2000);
		return;
	}
	alert("hello!");
}
foo();

最初は変数timerの値がfalseなので、fooが実行されると、if文の中でタイマーがセットされます。そして、一旦foo関数を抜け出します。次に2秒置いて、タイマーでセットされた関数が実行されます。ここで、まず最初にtimerの値をtrueにします。次に関数fooを、foo関数の中で、再帰呼び出しします。先ほどとは違い、timerの値がtrueになっていますので、timerはセットされずに、alertが実行され、「hello」と出力されます。そして、現在のfoo関数を呼び出している、foo関数に戻り、timerの値をfalseに戻しています。そして、alertが実行される前にreturnによって関数を抜け出しています。これの呼び出し階層をまとめると、以下のようになります。

-foo
  -setTimeout

-foo
  -foo
    -alert

ちなみにfoo()を連続して呼び出すと、2秒たってから、連続してアラートが表示されます。一番最初に紹介したようなforを二回重ねる方法の場合、連続して関数を呼び出すことはできず、二秒たってから一つ目のアラートが表示され、さらに二秒たってからアラートが表示されるという挙動になります。

なお、setTimeoutの中で指定した関数は、グローバルスコープで実行されます。そのため、this参照を使っていると、参照がうまくいかない場合があります。そんなときは、クロージャのテクニックを使ってください。