JS_Randezvousを高速化した
cho45さんがJSDeferredを高速化した手法を少し改良して、JS_Randezvousも高速化してみた。
具体的には、キューイングが遅いsetTimeout
(引数に0を渡しても大分待たされる)の代わりに、Image
オブジェクトのイベントを使う。
Image
オブジェクトの本来の用途は画像のプリフェッチだと思われる。src
プロパティを設定すると、オブジェクトをdocument
オブジェクトに追加しなくても自動で読み込み処理が開始されるため、document
オブジェクトに余計なノードを追加しなくても非同期なイベント(load
やerror
)を発生させることが出来る。読み込む画像ファイルのURLを空のdataスキームにすると、少なくとも現行のブラウザではsetTimeout
より数倍速く非同期処理を行うことが出来る。
src
属性に空のdataスキームを渡すと、大体のブラウザは画像ファイルを開くことが出来ないと判断してerror
イベントを発生させるようだ。ただし、例えばブラウザが画像を読み込まない設定になっている場合などは、src
属性の値に関係なくload
イベントを発生させることもあるらしい。手元で確認した範囲では、少なくともSafariはそのように動作する。JSDeferredの実装ではload
とerror
の両方をaddEventListener
することでこの問題を解決している。
ところで、dataスキームに対応していないIE6などではこの手法を利用することが出来ない。そこで、src
属性を空のjavascriptスキームにしたscript
要素をdocument
オブジェクトに追加することで非同期イベントを(setTimeout
に比べれば)高速に発生させている。イベントの発生はonreadystatechange
に登録されたハンドラで取得できる。ただし、このイベントは非同期ではあるものの、UIスレッドにスイッチする合間を持たないようなので、実際にはsetTimeout
と組み合わせて利用しないと、UIが更新されない。
さて、問題はこれらの手法をどう切り替えるかである。JSDeferredでは簡単なブラウザ判定を行って実行時に切り替えるような実装になっている。がしかし、この方法は若干怪しいような気もする。ブラウザの設定次第では正しく動作しないかもしれないので(UserAgent偽装など)、JS_Randezvousでは実際にイベントを発生させて確認するように改善してみた。
// 1度全部の方式をメソッドとして定義する。
Task.Statement.prototype.__execEnterDefault = function(task, args) {
setTimeout(this.__body(this, task, args), 0);
};
Task.Statement.prototype.__execEnterImageError = function(task, args) {
var img = new Image();
var that = this;
img.addEventListener("error", this.__body(this, task, args), false);
img.src= "data:,=";
};
Task.Statement.prototype.__execEnterImageLoad = function(task, args) {
var img = new Image();
var that = this;
img.addEventListener("load", this.__body(this, task, args), false);
img.src= "data:,=";
};
Task.Statement.prototype.__execEnterOnreadystatechange = function(task, args) {
var now = new Date();
var handler = this.__body(this, task, args);
if (now - task.__prevOnreadystatechange < 200) {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "javascript:";
script.onreadystatechange = handler;
} else {
setTimeout(handler, 0);
task.__prevOnreadystatechange = now;
}
};
// とりあえず、デフォルトを初期値として採用
Task.Statement.prototype.__execEnter =
Task.Statement.prototype.__execEnterDefault;
// 実際にイベントを取得できるようならば切り替える。
try {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "javascript:";
script.onreadystatechange = function () {
if (Task.Statement.prototype.__execEnter
!= Task.Statement.prototype.__execEnterDefault) return;
Task.Statement.prototype.__execEnter =
Task.Statement.prototype.__execEnterOnreadystatechange;
};
} catch (e) {
}
try {
var img = new Image();
img.addEventListener(
"error",
function() {
if (Task.Statement.prototype.__execEnter
!= Task.Statement.prototype.__execEnterDefault) return;
Task.Statement.prototype.__execEnter =
Task.Statement.prototype.__execEnterImageError;
},
false
);
img.addEventListener(
"load",
function() {
if (Task.Statement.prototype.__execEnter
!= Task.Statement.prototype.__execEnterDefault) return;
Task.Statement.prototype.__execEnter =
Task.Statement.prototype.__execEnterImageLoad;
},
false
);
img.src= "data:,=";
} catch(e) {
}
この方法ならば、若干コードが冗長になるにせよ、イベントが上手く発生しなくて困ることはないだろう。JSDeferredと異なり、error
とload
でメソッドを分けているのは念のためなので、ここまで細かく分ける必要はないかもしれない。なお、切り替え自体が非同期に行われるため、タイミングによっては初回だけデフォルトのsetTimeout
版が使われてしまうことも考えられる。しかし、次移行は高速版に切り替わっているはずなので、実際には問題はないはずだ。
なお、間に挟まっていたゆのさんはcho45さんの元に残してきたので安心である。