♪Come on baby, light my fire (c) The Doors

setTimeout()よりも早く確実に、かつ、DOMContentLoadedの発火を待たずに関数を実行

うちのサイトでは全般的なスタイルを指定したlayout.cssと、レガシー・ブラウザとスクリプト無効なブラウザ向けのlayout_sub.cssの2つのスタイルシートを用意している。 そしてモダン・ブラウザではDOMContentLoadedのタイミングでlayout_sub.cssを無効化している。

で、スタイルシートよりもスクリプトが後ろにあった場合、DOMContentLoadedは、スタイルシートの解析・適用が終わった後に 発火されることがある。

このため、通信環境の影響など何らかの理由でDOMContentLoadedの発火タイミングが遅れた時には、layout_sub.cssの指定が効いてしまって一瞬チラツク、という現象が時折起きていたorz

これをなんとかしたい、DOMContentLoadedよりもさらに早いタイミングでlayout_sub.cssを無効化したい。 普通に考えたらsetTimeout()を使うところなのだろうが、今の時代、それはなんだか野暮w。 それに、単にキューのウェイティングリストに送るだけだから、他の処理と重なったりしたら必ずしもDOMContentLoadedよりも早くなるとは限らない。

逆のアプローチで、レガシー・ブラウザ向けにlink要素を生成する方法もあるけど、これだとスクリプト無効なブラウザには使えない。 noscript要素もapplication/xhtml+xmlなうちのサイトでは使えない

…などとつらつら考えつつ、あらためてMDNのsetTimeoutのドキュメントを読み返していたら、かのDavid Baron氏のサイトのsetTimeout with a shorter delayという記事への誘導があった。

HTML5で策定されたpostMessage()を使う方法で、さすがdbaron御大、目から鱗が落ちた。

setZeroTimeoutを改訂 (最終更新:2015/4/30)

でも、元のスクリプトは、配列に関数を突っ込んだり引っ剥がしたりしているので、ほぼ同時並行的に複数の処理を走らせようとした場合とかに暴走する可能性があるかも、という懸念が少し残った(杞憂かもしれないけど…)。

だったら連想配列にすればいいじゃん、ということで、少し改訂したものが下記。

さらに、そのテストケース


/**
*   setZeroTimeout() (revised by Kazz)
*   Original: David Baron's weblog: setTimeout with a shorter delay
*   http://dbaron.org/log/20100309-faster-timeouts
*   Copyright (c) 2014-2015 Kazz
*   http://asamuzak.jp
*   Dual licensed under MIT or GPL
*   http://asamuzak.jp/license
*/

(function(_win) {
    'use strict';
    function isType(o, t) {
        var x = o && Object.prototype.toString.call(o).slice(8, -1);
        return o && (t ? new RegExp('^' + t + '$', 'i').test(x) : x);
    }

    var _Z = {};
    function setZeroTimeout(o, p) {
        isType(o, 'Function') && isType(p, 'String') && (!_Z[p] && (_Z[p] = o), _win.postMessage(p, '*'));
    }
    function clearZeroTimeout(p) {
        isType(p, 'String') && _Z[p] && (_Z[p] = null);
    }
    function handleZeroTimeoutMessage(evt) {
        var x = evt.data, y = _Z[x];
        y && evt.source === _win && (evt.stopPropagation(), clearZeroTimeout(x), y());
    }
    _win.addEventListener('message', handleZeroTimeoutMessage, true);
    _win.setZeroTimeout = setZeroTimeout;
    _win.clearZeroTimeout = clearZeroTimeout;
})(window);

/* call setZeroTimeout() like below */
function myFunc() {
    /* do something */
}
setZeroTimeout(myFunc, 'myFunc');

元のスクリプトとの主な違いは…

  • 連想配列にするため、タイマー代わりのArrayはObjectに変更
  • setZeroTimeout()は、第1引数で「関数」、第2引数で「関数名」(連想配列のキーとして使用)
    なお、第2引数は、固有のキーとして識別可能であれば別に関数名でなくても構わない
  • postMessage()するのは固定メッセージではなく、個々のキーに変更

これで心置きなくsetZeroTimeoutを好きなだけ好きなタイミングで使えるようになった…気がするw

setZeroTimeoutとDOMContentLoadedのどちらか早い方で実行する方法

次に、文書のロード時にできるだけ早いタイミングで関数を実行させるにはどうすればいいか?

実際にstyle要素やlink要素の後にscript要素を置いているケースで簡単に試した限りでは

  • Firefoxは、setZeroTimeoutを使った方がDOMContentLoadedより早く実行される
  • Chromeでは、最初の読み込み時にはsetZeroTimeoutの方が早い(場合が多い)が、リロードなどでキャッシュが効いている場合はDOMContentLoadedの方が早く実行される
  • 一方、IE11では、DOMContentLoadedの方が早く実行される

という結果になっていた。

そこで、setZeroTimeoutとDOMContentLoadedのどちらか早い方で実行するならこんな感じかな?


(function(_win, _doc) {
    'use strict';
    function isType(o, t) {
        var x = o && Object.prototype.toString.call(o).slice(8, -1);
        return o && (t ? new RegExp('^' + t + '$', 'i').test(x) : x);
    }

    var _Z = {};
    function setZeroTimeout(o, p) {
        isType(o, 'Function') && isType(p, 'String') && (!_Z[p] && (_Z[p] = o), _win.postMessage(p, '*'));
    }
    function clearZeroTimeout(p) {
        isType(p, 'String') && _Z[p] && (_Z[p] = null);
    }
    function handleZeroTimeoutMessage(evt) {
        var x = evt.data, y = _Z[x];
        y && evt.source === _win && (evt.stopPropagation(), clearZeroTimeout(x), y());
    }
    _win.addEventListener('message', handleZeroTimeoutMessage, true);
    _win.setZeroTimeout = setZeroTimeout;
    _win.clearZeroTimeout = clearZeroTimeout;

    /* the way to run function as soon as possible */
    function myFunc() {
        /* do something */
    }
    function runFnAsap(evt) {
        var x = someObj; // target object or trigger object
        if(evt && evt.type === 'DOMContentLoaded') {
            if(x) {
                clearZeroTimeout('myFunc');
                myFunc();
            }
        }
        else if(!evt) {
            if(x) {
                _doc.removeEventListener('DOMContentLoaded', runFnAsap, false);
                myFunc();
            }
            else {
                setZeroTimeout(runFnAsap, 'runFnAsap');
            }
        }
    }
    _doc.addEventListener('DOMContentLoaded', runFnAsap, false);
    setZeroTimeout(runFnAsap, 'runFnAsap');
})(window, document);
  • runFnAsap()をsetZeroTimeoutとDOMContentLoadedで2度呼ぶ
  • runFnAsap()の中では、DOMContentLoaded時の処理とsetZeroTimeoutの処理を場合分け
  • DOMContentLoadedの方が早く発火した場合は、setZeroTimeoutのタイマー代わりのキーをクリア
    一方、setZeroTimeoutの方が早く発火した場合はイベントリスナを解除
    なお、いずれも、別の真偽値判定などを併用して2回走らせても構わない場合には必ずしも必要ない
  • まだ対象オブジェクトが読み込まれていなかったら、setZeroTimeoutで再帰処理

んでもってlayout_sub.cssは…

そして、うちのサイトのlayout_sub.cssを無効化するスクリプトはこんな形に落ち着いた。


(function(_win, _doc) {
    'use strict';
    function isType(o, t) {
        var x = o && Object.prototype.toString.call(o).slice(8, -1);
        return o && (t ? new RegExp('^' + t + '$', 'i').test(x) : x);
    }

    var _Z = {};
    function setZeroTimeout(o, p) {
        isType(o, 'Function') && isType(p, 'String') && (!_Z[p] && (_Z[p] = o), _win.postMessage(p, '*'));
    }
    function clearZeroTimeout(p) {
        isType(p, 'String') && _Z[p] && (_Z[p] = null);
    }
    function handleZeroTimeoutMessage(evt) {
        var x = evt.data, y = _Z[x];
        y && evt.source === _win && (evt.stopPropagation(), clearZeroTimeout(x), y());
    }
    _win.addEventListener('message', handleZeroTimeoutMessage, true);
    _win.setZeroTimeout = setZeroTimeout;
    _win.clearZeroTimeout = clearZeroTimeout;

    /*
    *   toggleStyle()
    *   スタイルシートの適用を切り替え
    */
    function toggleStyle(evt) {
        function disableStyleSheet(o) {
            !o.disabled && (o.disabled = true, clearZeroTimeout('toggleStyle'));
        }
        var x;
        if(_win.requestAnimationFrame) {
            x = _doc.querySelector('link[href$="layout_sub.css"]').sheet;
            evt && evt.type === 'DOMContentLoaded' ? (x && disableStyleSheet(x), /* さらにモダン・ブラウザ用の他の処理 */) : !evt && (x ? disableStyleSheet(x) : setZeroTimeout(toggleStyle, 'toggleStyle'));
        }
    }
    _doc.addEventListener('DOMContentLoaded', toggleStyle, false);
    setZeroTimeout(toggleStyle, 'toggleStyle');
})(window, document);

これで無事に懸案は解消できた(…はず)

"♪Come on baby, light my fire (c) The Doors"へのTwitter上でのコメントやRT

6件のツイートがあります。

ツイート 1

ツイート 2

ツイート 3

ツイート 4

ツイート 5

ツイート 6