Admittedly something small.

ちょっと小さいのはたしかですが。

なるべく早くtimeoutする

2015年11月1日 ( 9年前に投稿 )

JavaScript

何十秒もかかる重いタスクがあり、それをブラウザ上のJavaScriptでなるべく早く完了させたいとします。でも同期的に一気にやろうとすると、たまには制御返せってブラウザに怒られるし、ページが固まってしまうので困ります。

たとえば、どうしてもコラッツの問題を自分で確かめたくなっちゃったとします。nに操作を繰り返して1に到達するまでの回数をcollatz(n)として、たとえば次のように一気にfor文で調べようとするとページ全体が固まってしまい、ユーザの操作で途中で止めることもできないのでよろしくありません。

for(var n = 1; n <= 10000000; n++){
    console.log(n + "は" + collatz(n) + "回の操作で1に到達します");
}

ベストなのはWebWorkersを使って別スレッドで処理することですが、Workerスレッド上ではDOMに触れないとかいろいろ制限があります。タスクの内容によってはWorkerの上ではできない、もしくは面倒くさいこともあるでしょう。

そのようなときには、タスクを分割してsetTimeoutsetIntervalで非同期に処理する方法があります。コラッツの問題ならただの数値計算なのでWeb Workers上でやればいいですが、仮にこの問題がメインスレッド上でないとやりにくいタスクだったとします。具体的には次のような関数sleepを用意すると現代的でちょっとおしゃれかもしれません。

function sleep(){
    return new Promise(resolve => setTimeout(resolve, 0));
}

呼び出す側ではジェネレータ関数の中で

for(var n = 1; n <= 10000000; n++){
    console.log(n + "は" + collatz(n) + "回の操作で1に到達します");
    yield sleep();
}

とループごとに一休みするだけで、ページ全体をブロックすることなく処理を行えます。ただしsetTimeoutを使うと、setTimeout(f, 0)としてdelayを0ミリ秒と指定しても最低でも数ミリ秒程度の遅延が生じます。とにかく早く終わらせたいときにはこの遅延が邪魔になるのですが、postMessageでメッセージを投げる → onmessageで即座にイベントを受け取ってタスクをこなす、というトリックを使うことで高速に非同期処理が繰り返せます。

こちらの実装のコードを借りると、次のsetZeroTimeout関数はほとんど瞬時に、でもあくまで非同期にコールバックしてくれます。

// Only add setZeroTimeout to the window object, and hide everything
// else in a closure.
(function () {
    var timeouts = [];
    var messageName = "zero-timeout-message";

    // Like setTimeout, but only takes a function argument.  There's
    // no time argument (always zero) and no arguments (you have to
    // use a closure).
    function setZeroTimeout(fn) {
        timeouts.push(fn);
        window.postMessage(messageName, "*");
    }

    function handleMessage(event) {
        if (event.source == window && event.data == messageName) {
            event.stopPropagation();
            if (timeouts.length > 0) {
                var fn = timeouts.shift();
                fn();
            }
        }
    }

    window.addEventListener("message", handleMessage, true);

    // Add the one thing we want added to the window object.
    window.setZeroTimeout = setZeroTimeout;
})();

このsetZetoTimeoutsetTimeoutの代わりに使うだけでずっと早く処理が終わります。collatz(n)のリアルタイム人気投票をするとこんな感じになります。ただし、setTimeoutにはタブが裏に回るなどでページが非表示になると自動的に頻度を落として無用な処理を減らしてくれるという機能もあります。setZeroTimeoutのほうはページが非表示だろうがお構いなしに全力疾走してしまいますが、それが迷惑な場面もあるでしょうから注意が必要です。使い道があるようなないような……。

参考

  • http://dbaron.org/log/20100309-faster-timeouts
  • https://developer.mozilla.org/ja/docs/Web/API/Window/setTimeout

(この記事は同じ筆者が Qiita に投稿した記事の複製です。オリジナル記事)