2006年09月23日 01:30 [Edit]

javascript - ページはいつ再描画されるか

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

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


この記事へのトラックバックURL

この記事へのトラックバック
404 Blog Not Found:javascript - ページはいつ再描画されるか
404 Blog Not Found:javascript - ページはいつ再描画されるか【】at 2012年01月16日 11:19
そんな方法があったのか、大して用もないのにconfirmとかalertでメッセージ出して最描写させてたよ……orz。( ..)φメモメモ
betaNode::2006-09-23【betaNode】at 2006年09月23日 03:50
この記事へのコメント
Smoking: Studies indicate that men who smoke have a greater chance of developing ED than men who dont use tobacco. <a href= http://procalisx.myforum.ro >4men sexual problem</a><br> [url=http://procalisx.myforum.ro]4men sexual problem[/url]<br>
Posted by greg at 2007年02月16日 02:02
Partners were satisfied with how well ProCalisX improved erections of their men with ED <a href= http://procalisx.myforum.ro >4men health</a><br> [url=http://procalisx.myforum.ro]4men health[/url]<br>
Posted by greg at 2007年02月15日 22:19
ループの中にalertを入れると毎回再描画されるわけですが。
Posted by umitanuki at 2006年09月23日 21:55
でも逆にシングルタスクだからこそ資源(主にDOMツリー)のレースコンディションを意識せずにプログラムを書いたり、いろんなライブラリを混在できたりするってのも事実ではないでしょうか。

window.refresh()は欲しいですね。
でもこれいつ戻ってくるか保証できないので実装が辛そう?とも思いました。
Posted by ajiyoshi at 2006年09月23日 16:58
結局preemptiveではないんだけど、もうちとましにしようとしているのが yield の実装なのかしら。

[javascript] JavaScript 1.7 の yield が凄すぎる件について
http://d.hatena.ne.jp/amachang/20060805

まあこの辺は言語仕様というよりはインタプリタの実装側がんばれよ、という話なのかもしれないけど。
Posted by くりやま at 2006年09月23日 14:07
setInterval を使えばマシになるかと思ったけど,this をエレメントにするために prototype.js のような bind が必要になるからイマイチかなぁ..
<pre>
<script>
Function.prototype.bind = function(obj) {
var __method = this;
return function() {
return __method.call(obj);
}
}

var count = 99;
function good_count() {
if (--count > 0) {
this.value = count;
}
else {
clearInterval(timer);
this.value = 'count';
}
}

</script>
<input type="submit" value="count" onclick="timer = setInterval(good_count.bind(this), 10);">
</pre>
Posted by teahut at 2006年09月23日 11:40
先生ヘルプw
Posted by ハンカチ王子 at 2006年09月23日 08:28
どうすりゃ、いいんでしょうか?
Posted by クモ at 2006年09月23日 06:59