スポンサーリンク

JavaScriptの動かないコード (中級編) setTimeoutのタイマーが指定時刻に動かないエラー (JavaScriptがマルチスレッドだという誤解)


以下のJavaScriptコードが意図した動作をしないのは,なぜですか。(制限時間1分)


やりたい事:

  • 0から20000までループする。
  • そのループの途中にタイマーを設定して,ループの進捗を表示する。

<input type="button" value="ループを開始する" onClick="f()">

<!-- ここにループ回数が表示されます。 -->
<div id="div_counter"></div>

<!-- ここに進捗状況のコメントが表示されます。 -->
<div id="div_comment"></div>

<script language="JavaScript">

function f()
{
	// ループ用のカウント
	var i = 0;

	// 1秒後にタイマーを設定(ループの途中で実行したい)
	setTimeout(
		function(){
			div_comment.innerHTML += "1秒経過しました。ループの現在の進捗:i = "
				+ i
				+ "<br>時刻:" 
				+ (new Date()).getTime()
				+ "<br>"
			;
		},
		1000
	);

	// 2秒後にタイマーを設定(ループの途中で実行したい)
	setTimeout(
		function(){
			div_comment.innerHTML += "2秒経過しました。ループの現在の進捗:i = "
				+ i
				+ "<br>時刻:" 
				+ (new Date()).getTime()
				+ "<br>"
			;
		},
		2000
	);
	
	// ループを開始する
	for( ; i < 20000; i ++ ){

		// カウントを表示します
		div_counter.innerHTML = i;

	}
	div_comment.innerHTML += "ループの実行が終わりました。<br>時刻:"
		+ (new Date()).getTime()
		+ "<br>"
	;
}

</script>




答え



ボタンを押すと数秒間かけてループが最後まで走り,「ループの実行が終わりました」と表示されてしまう。

そのあとで,「1秒経過」「2秒経過」が立て続けに表示される。



本当はループが終わる前に,その途中で進捗を表示させたかったのだが。



setTimeoutで設定したタイマーが,思った通りの秒数後に実行されていない。


追及してみる

上記のコードに「0秒後」というタイマーを追加した上で,再度実行してみると下記のようになる。



IEでの表示:

ループの実行が終わりました。
時刻:1244948218671
0秒経過しました。ループの現在の進捗:i = 20000
時刻:1244948218687
1秒経過しました。ループの現在の進捗:i = 20000
時刻:1244948218687
2秒経過しました。ループの現在の進捗:i = 20000
時刻:1244948218687


Firefoxでの表示:

ループの実行が終わりました。
時刻:1244947872888
0秒経過しました。ループの現在の進捗:i = 20000
時刻:1244947873115
1秒経過しました。ループの現在の進捗:i = 20000
時刻:1244947873329
2秒経過しました。ループの現在の進捗:i = 20000
時刻:1244947873330


どちらのケースでも,

  • ループの終了後,200ミリ秒程たってから「0秒経過」が出る
  • その後,ごく短い時間(0〜200ミリ秒)がたってから「1秒経過」
  • その後,極めて短い時間(0〜1ミリ秒)がたってから「2秒経過」

と出ている。



これはあたかも,

  • タイマーの指定時刻が到来しているのに,
  • ループ中は,タイムアウト時の処理本体の実行に「待った」がかけられている
  • ループ終了後に一斉にタイムアウト処理が実行される(=待たされていた処理達が,実行キューから一斉に取り出される)

ようだ。


原因


原因は下記の2つ。

  • JavaScriptがシングルスレッドであるということと,
  • setTimeoutは
    • 「指定秒数後に実行」ではなく
    • 「指定秒数後に実行キューに登録」(その場で実行ではなく,キューの中で順番がまわってくるのを待つ必要がある)ということ。

JavaScriptがウェブを遅くする--今できる緩和策を考える(→1つのウィジェットの読み込みが遅ければ,ブログ全体の読み込みが重くなりうる)
http://japan.cnet.com/column/rwweb/st...

ブラウザはJavaScriptをどう処理するか

 経験を積んだ技術者にとって最もショッキングな事実は、JavaScriptがシングルスレッド型の言語だということかもしれない。
これは、 JavaScriptが物事を同時にではなく順番に処理するということだ(Ajaxの呼び出しは例外である)。
JavaScriptの断片がロードされ、あるいは評価されているとき、他のすべての処理はその終了を待たなくてはならない。
これは、遅いJavaScriptが1つあれば、ブログ全体の読み込みが遅くなってしまう場合があるということだ。


JavaScript を学ぶ際に一番重要なのに、誤解されがちな setTimeout 系の概念
http://d.hatena.ne.jp/amachang/200609...

JavaScript には実行キューがあり、setTimeout は指定秒後にそれに関数を登録している、ということ

1つのループ処理が重ければ,順番を待たされているイベントの実行はpendingになり,実行が遅れる。



冒頭のコードの派生形は,「setTimeout問題」として知られている。

alertやpromptなどのモーダル処理が現れると,そこで実行キュー内の後続処理の順番待ちが解除される,というもの。

javascriptは本当にシングルスレッドで実行されているけれど起こるsetTimeout/prompt現象
http://labs.gmo.jp/blog/ku/2007/09/fi...

confirmのときにはconfirmを呼び出したときにイベントがpending状態に変わって後続のイベントが実行されるのに対して、時間のかかる処理のときには実行中のイベントがpending状態に切り替わることがないのでタイムアウトハンドラが呼ばれない。


仕様への誤解が生むエラーと言える。