大変に有用な考察だが、一つ重要な指摘漏れがある。

IT戦記 - JavaScript を学ぶ際に一番重要なのに、誤解されがちな setTimeout 系の概念
setInterval、setTimeout、イベントによる関数の実行を理解することだと思う

ページがいつ再描画されるか、ということである。

未経験者は、document.write()element.innerHTML = "foo"のように、ブラウザーに「書き出した」点でそれが直ちに反映されると思うだろう。

ところが、そうではないのである。

実例を見てみよう。以下のscriptを考えてみる。ボタンを押すと、ボタンのラベルが1000から1までカウントダウンした後、元通りになることを意図しているように見える。

<script>
function bad_count(evt){
  var self = evt.target || evt.srcElement;
  for (var count = 1000; count > 0; count--){
    self.value = count;
  }
  self.value="count";
}
</script>
<input type="submit" value="count" onclick="bad_count(event)">

ところが、このボタンは期待通り動かない。クリックしても何も起きないようにしか見えない。

それはなぜか?ここに秘密がある。

関数の実行中は、ページの書き換えは起こらない。すなわち、関数の実行キューが空になって時点ではじめてページの書き換えが起こるのだ。だから上記の例では、最終実行結果のみが表示されるというわけだ。

現在のブラウザーの実装は、以下のようになっているようである。「ようである」というのは、仕様書で確認したわけではないからだ。

イベント発生 → 関数の実行 → ページの再描画 

上記の例で意図したように、カウンターが動いた都度表示させたいと思ったら、その都度関数の実行を完了しなければならない。しかしカウンターを動かすという行為そのものに関数の実行が必要である。どうしたらよいか?

その答えが、setTimeout()というわけだ。上記の例では、カウンターを一つ動かしたら、次にカウンターを動かす関数をsetTimeout()で登録する。すると指定時間以上後にその関数を実行せよというイベントが発生し、またその関数が実行され、ページが再描画される。

それを実際にやるとどうなるか?以下のようになる。

<script>
var count;
var timer;
function good_count(evt){
  var self = evt.target || evt.srcElement;
  count = 1000;
  var callback = function(){
    self.value = count--;
    if (count > 0){
       timer = setTimeout(callback, 0);
    }else{
       self.value = "count";
    }
  }
  timer = setTimeout(callback, 0);
}
</script>
<input type="submit" value="count" onclick="good_count(event)">

今度はちゃんと動く。ちゃんと動くが、このややこしさは一体何だろう。こうしたコールバック関数を使ったイベント処理というのは、GUIプログラミングを経験した人であればある程度感触がつかめると思うが、逐次的に処理されるプログラムしか書いたことがない人には「ハァ?」の世界だろう。

しかしこのおかげで、プログラムを「実行中」に中断することも可能になる。以下はカウンターが動いている間にボタンをクリックするとカウントが中断する例だ。

function better_count(evt){
  var self = evt.target || evt.srcElement;
  if (timer){ 
    timer = clearTimeout(timer); // clearTimeout(timer); timer = undefined;
    return;
  }
  if (! count) count = 1000;
  var callback = function(){
    self.value = count--;
    if (count > 0){
       timer = setTimeout(callback, 10);
    }else{
        timer = clearTimeout(timer);
       self.value = "count";
    }
  }
  timer = setTimeout(callback, 10);
}

とはいえ、このやり方は極めて非直感的で、バグの温床ともなりうる。特にコールバック関数を書く時には、「終了」処理をちゃんとやらないと首を傾げるバグの発生源となる。上の例の後のclearTimeout()を書き加えておかないと、「実行終了」後に二度クリックしないと再カウントしない羽目になる。

実際、Javascriptにおけるこうした処理というのは、やはりpreemptive multitaskingな環境と比較すると正直書きづらいと思う。おかげでUnixのsleep()程度のことでも知恵を絞らなくてはならない。おっさんとしての印象では、OS Xになる前のSystem 7以降のMac OSの感覚に似ている。当時はそれをcooporative multitaskingと呼んでたっけ。自前で処理をOSに返さないと、いつまでたっても残りのプログラムは待たされる羽目になる点において、現在のブラウザーにそっくりなのだ。

ブラウザーがOSになるご時世である。これらもpreemptiveになって欲しいと思うのは私だけだろうか。少なくとも、Unixのfflush()的なものは欲しい。関数の中でもそれを呼ぶと強制的にページ再描画されるような関数が。window.refresh()。とか。

Dan the Javascripter