Secrets of the JavaScript Ninja 6章を読んで来た

今回は6章(Timers)です!前回と同じく予習の時にjsのタイマーについて
少し日本語情報を仕入れてから読み始めました.

概要

  • jsはシングルスレッド
  • タイマーはjsの機能ではなくてブラウザ側の機能

タイマーの働き

  • jsでタイマーを実現するための関数は下の4つ
    • setTimeout
      • 1回きり
    • setInterval
      • 定期実行
    • clearInterval
      • setIntervalで指定したタイマー解除
    • clearTimeout
    • setTimeoutで指定したタイマー解除

setTimeoutもsetIntervalも戻り値があって,戻り値がユニークなIDであることで,複数タイマーを使ったときにあのタイマーだけを消したいってことができるようになってるんですね!
似たような例として,phpmysqlに接続したときに帰ってくるリソースのようなイメージだと思います.

setTimeoutとかsetIntervalを使うと何秒後に実行する処理が書けると思っていたのですが,jsがシングルスレッドであるという背景から,実際は下の図のように処理がに実行待ちキューに格納されて,前の処理が終わるまで実行されません.

図の例だと、jsの処理、マウスクリックイベント、タイマー処理、タイマーによる定期処理がそれぞれあったときに,セットしていた時間よりも遅れて実行されるということが分かります.

タイマーの最小遅延と信頼性の話

setTimeoutの第二引数に指定する遅延時間(ms単位)は,毎回同じではなく,誤差があって平均して10-15msくらいだそうです.また,OS、ブラウザによってもタイマーの精度が異なっていて0msの遅延としても,MacOSXだと10ms、Windowsは15msくらいの遅れがおこるとのこと.
ちなみにIEは0msと指定すれば0msで動くようですw我が道を行ってますね...
このようにプログラマはタイマーには誤差があるんだということを頭に入れてアプリを書く必要があります.

重い処理の話

jsで時間のかかる処理(DOM要素を動的に生成,複雑なアニメーションなど)をすると,ブラウザが固まってしまい無反応になることがあります.Macだと虹が出たり...
iPhoneだと5秒以上処理に時間がかかるとスクリプトの実行を止めてしまいます.確かにiPhoneで試してみたら結果が表示される前に反応が返ってきて,真っ白な画面が表示されました.
jsはブラウザの入出力を担うUIビューというスレッド上で実行されるそうなのですが(これは本に書いてあったわけではなくてid:cheesepieさん情報です),
jsの重い処理が終わるまで描画処理が行えないため,ブラウザが固まるようになってしまうそうです.

本章で紹介されていたのは,

<table><tbody></tbody></table>
<script>
// Normal, intensive, operation
var table = document.getElementsByTagName("tbody")[0];
for ( var i = 0; i < 2000; i++ ) {
  var tr = document.createElement("tr");
  for ( var t = 0; t < 6; t++ )
    var td = document.createElement("td");
    td.appendChild( document.createTextNode("" + t) );
    tr.appendChild( td );
  }
  table.appendChild( tr );
}
</script>

ような,tableの内容を動的に書き出すような処理(26,000のDOM要素生成→1(trタグ)+6(tdタグ)+6(tdの中の文字) x 2000)
をしたときに,UIをブロックせず(正確にはユーザには分からないように!?)に処理を実行するときにタイマーが使えます.タイマーを使ってUIをブロックしない単位に分割して処理を実行します.
上の例をタイマーを使って書きかえた例が,

<table border="1"><tbody></tbody></table>
<script>
var table = document.getElementsByTagName("tbody")[0];
var i = 0, max = 1999;

setTimeout(function(){
  for ( var step = i + 500; i < step; i++ ) {
    var tr = document.createElement("tr");
    for ( var t = 0; t < 6; t++ ) {
      var td = document.createElement("td");
      td.appendChild( document.createTextNode("" + t) );
      tr.appendChild( td );
    }
    table.appendChild( tr );
  }

  if ( i < max) {
    settimeout( arguments.callee, 1000 );
  }
}, 1000);
</script>

です.何をしているかというと、先ほどの26,000のDOM作成処理のループを4分割して,ブラウザが無反応になる前に処理を終わらせしまうことで,ユーザには軽い処理のように見せています.

複数タイマーの管理の話

アニメーションなどのタイマーを複数使うような処理を各場合に,タイマーを集中管理できるような仕組みを作っておかないと、タイマーのクリア漏れが起きたりして処理が暴走してしまうということでした.
集中管理できるような仕組みがあることで,

  • 1度に1つだけのタイマー処理だけ気にしていればよく,
  • 意図したようにタイマーをストップ,スタートすることができ,
  • callback関数を取り除く過程を簡略化できる

という利点があります.

本章で紹介されていた例は,

<div id="box" style="position:absolute;">Hello!</div>
var timers = {
    timerID: 0,
    timers: [],
    start: function(){
        if ( this.timerID )
            return;

        (function(){
             for ( var i = 0; i < timers.timers.length; i++ )
                 if ( timers.timers[i]() === false ) {
                     timers.timers.splice(i, 1);
                     i--;
                 }
             timers.timerID = setTimeout( arguments.callee, 0 );
         })();
    },
    stop: function(){
        clearTimeout( this.timerID );
        this.timerID = 0;
    },
    add: function(fn){
        this.timers.push( fn );
        this.start();
    }
};
var box = document.getElementById("box"), left = 0, top = 20;

timers.add(function(){
	box.style.left = left + "px";
	if ( ++left > 50 )
	   return false;
});

timers.add(function(){
	box.style.top = top + "px";
	top += 2;
	if ( top > 120 )
	   return false;
});

のように,このtimersオブジェクトを使って遅延処理をすることで,addメソッドを使って処理を追加していくだけになるというプログラムでした.
でも,このプログラム、バグがありました!
stopメソッドが定義されているのに,どこからも呼ばれていません...
MacOSXのFirefox3.6.3, Safari4.0.5, Opera10.53, Chrome5.0.375.55でarguments.calleeの行の前にconsole.logを差し込んで実行してみたところ,Firefox以外のブラウザではsetTimtoutが止まらず延々と実行されていました.FirefoxはよしなにsetTimeout忘れを処理してくれるんですね...
これはもうちょっと調べて別エントリに挙げようと思います.

非同期テストの話

2章でやった内容とほぼ同じなのでとばしました!

次回以降の読む順序

この後は
7章: Regular Expressions
8章: With Statements
9章: Code Evaluation
10章: Strategies for Cross-Browser Code
11章: CSS Selector Engine
12章: DOM Modification
13章: Attributes and CSS
14章: Events
と続くのですが,7, 8, 9, 11章はライブラリとか作る人用の内容だから
後回しにしようということで,

10→12→13→14→11→7→8→9→11

の順番で読んでいくことになりました.

ということで次回は10章(Strategies for Cross-Browser Code)です!