JavaScriptはほんとにシングルスレッドで実行されているのか?

amachang氏の
JavaScript を学ぶ際に一番重要なのに、誤解されがちな setTimeout 系の概念 - IT戦記
を読んで、「そうそうJavaScriptはシングルスレッドだからね〜」なんて思っていながらその実証コードを作って遊んでいるうちに奇妙な現象を発見した。

以下が問題のコード

<html>
<head>
<script type="text/javascript">
function test() {
  window.str = undefined;
  setTimeout(function(){
    alert(window.str);
  }, 0);
  window.str = prompt('hoge?');
}
</script>
</head>
<body onclick="test()">
</body>
</html>

ドキュメントのbodyにonclickハンドラを登録して、そこで0ミリ秒指定でsetTimeoutを呼び出し、その後にsetTimeoutに登録した関数で使っている変数をprompt関数を使って初期化しています。promptはダイアログが表示され、「OK」ボタンを押すまで処理を返しません。

このコードで示したかったのは、「setTimeoutに0ミリ秒指定で登録しても、promptの実行が完了するまでsetTimeoutの処理は実行されない」ということ。実際、IEOperaで実行すれば期待通りに動作します。
しかし、Firefox(1.5.0.6 Win)で試してみるとなんと、promptとalertが同時に表示されてしまいました。



promptとalertが同時に表示されてるよー!

これはどういうことでしょう?test関数とsetTimeoutに登録した関数が別スレッドで実行されているのでしょうか?

そこで、test関数を以下のように変更してみます。

<html>
<head>
<script type="text/javascript">
function test() {
  window.str = undefined;
  setTimeout(function(){
    alert(window.str);                        // ☆1
    window.str=prompt('timeout',window.str);  // ☆2
  }, 0);
  window.str = prompt('hoge?');  // ★1
  alert('test:'+window.str);     // ★2
}
</script>
</head>
<body onclick="test()">
</body>
</html>

これをFirefoxで実行すると、最初のコードと同様に★1のpromptと☆1のalertが同時に表示されます。
ここで、★1のpromptに文字を入力してOKを押しても★2のalertは表示されませんでした。次に、☆1をOKすると☆2が表示されますが、window.strの表示は"undefined"のままです。☆2をOKするとようやく★2のalertが表示されます。表示の内容は★1で入力した内容でした。
つまり、★1のpromptが表示された時点で処理がsetTimeoutに指定した関数に移って、その処理が終わってからようやく★1のpromptの戻り値がwindow.strに代入されたようです。
また、もしtest関数とsetTimeoutに登録した関数が別スレッドで並列に実行されているのなら、★1をOKした時点で★2が表示されるはずなので、やはり処理自体はシングルスレッドで実行されているものと思います。*1


さらにコードを以下のようにtest関数をscriptタグ内で直接実行するように変更するとこの現象は発生しなくなりました。

<html>
<head>
<script type="text/javascript">
function test() {
  window.str = undefined;
  setTimeout(function(){
    alert(window.str);
  }, 0);
  window.str = prompt('hoge?');
}
test();
</script>
</head>
<body></body>
</html>

また、promptの代わりに単純に処理に時間のかかる関数を実行した場合も発生しません。


以上のことから、どうやらFirefoxでは、イベント処理の最中にpromptやalert, confirmのようなダイアログを表示させる関数を実行してスクリプトを待ち状態にすると、setTimeoutなどの処理の割り込みが起こるようです。

結論としては、amachang氏の

var a;
var id = setTimeout(function() { alert(a) }, 10);
ここに、複雑な a の初期化処理を書く

こうしたときに、 a は確実に初期化されてることが保証される。

は「複雑な初期化処理」にprompt, alert, confirmのが含まれないことが必要となりそうです。(Firefoxの場合)

*1:実際には別スレッドだけど、同時には1スレッドしか動いていないのかもしれない。けど、それはスクリプトを書く側にとってみればシングルスレッドで実行されているのと変わらない。