nmi.jp Twitter → @tkihira
戻るボタンが押された際、ページのJavaScriptを途中の状態から実行させない方法 新しいコンピュータのセットアップ(Windows7編)

setTimeoutの基本


2011-06-07
Takuo Kihira

setTimeoutは、実行された時間より、指定されたミリ秒待機して実行します。ブラウザから提供されているAPIであり、ECMAScriptの仕様の範囲外です。JavaScriptのsetTimeoutは、ブラウザの実装を知っていると広く応用の効く命令となります。

待機してから実行する命令にはsetIntervalとsetTimeoutがありますが、setIntervalは使いづらいので特別な理由のない限り使わない方がよいでしょう。例えば

var cb = function() {
    var d = new Date();
    document.body.innerHTML = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();
    if(d.getSeconds() == 0) {
        clearTimeout(id);
    }
};
var id = setInterval(cb, 1000);

上のコードの問題点として、idという変数を使わないとクリア出来ないことと、setIntervalを実行した瞬間には描画されない問題があります。そこでsetTimeoutに書き直すと、

var cb = function() {
    var d = new Date();
    if(d.getSeconds() != 0) {
        setTimeout(cb, 1000);
    }
    document.body.innerHTML = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();
};
cb();

このように理解しやすく書くことが出来ます(名前付き関数式を使えばもっと簡潔に書けますが、IEのバグを踏む危険もあるのでここでは使っていません)。setIntervalを使った方が良い場面というのはほとんどないので、積極的にsetTimeoutを使うようにしましょう。

このような形でsetTimeoutを使う場合、一つ注意があります。真っ当なプログラマの感覚的に、setTimeoutを下の方に書いてしまいがちなのですが、関数のどこの位置にsetTimeoutを書くのかというのは性能に大きな影響を与える問題ですので、目的に合った場所を選択しなければいけません。

たとえばゲームなどで、60fpsを達成したいと思っている場合に、次の様なコードはいけません。

<html><head><title>setTimeoutの悪い例</title>
<script>
window.onload = function() {
    var count = 0;
    var gameMain = function() {
        // 何か重い処理をする
        var r = 0;
        for(var i = 0; i < 100000; i += 1.001) {
            r = i + i * r;
        }
        // 後方で呼び出す
        if(count >= 0) {
            setTimeout(gameMain, 1000 / 60);
            count++;
        }
    };
    var start = new Date().getTime();
    setTimeout(function() {
        var fps = (count / 3);
        document.body.innerHTML = "fps:" + fps;
        count = -1;
    }, 3000);
    gameMain();
};
</script></head><body></body></html>

この位置にsetTimeoutがあると、重い処理が終わった後から1000/60ms後に呼ばれてしまうので、結果として秒間に60回未満しか呼ばれなくなってしまいます。これを、次の様に書き換えるだけで60fpsに大きく近づきます。

var gameMain = function() {
    // 先頭で呼び出す
    if(count >= 0) {
        setTimeout(gameMain, 1000 / 60);
        count++;
    }
    // 何か重い処理をする
    var r = 0;
    for(var i = 0; i < 100000; i += 1.001) {
        r = i + i * r;
    }
};

実際のコードをリンクにて紹介します(悪い例良い例)。悪い例よりも良い例のほうが60fpsに近いことを確認していただけるのではないかと思います。

※最近のJavaScriptの実装で初回ロード時にJIT処理をする場合や、タイミング悪くガベージコレクションが走った場合など、悪い例の方が良い例よりも良いパフォーマンスを出すこともあります。もしくは速度が速すぎてほとんど区別が出来ない、などもあるかもしれません。そういうものだと思ってください。

なお余談ですが、上記の事を理解した上でも、やはり最後にsetTimeoutを書くのが望ましいこともあります。もしくは、例えば処理内容が1000/60秒で終わらない可能性が高い場合に、しかしブラウザのレンダリングエンジンを正確に回したい、というような状況では、上記のテクニックとは別種の方法で解決するのが望ましいでしょう。これについてもいずれ書いてみたいと思います。