window.setInterval は無限ループしたり実行順を保証できない場合がある

window.setInterval の問題点

window.setInterval には window.setTimeout にない2つの問題点があります。

  • 致命的なエラーが発生してもタスクを繰り返し実行する。
  • 2秒かかるタスクを1秒おきに予約すると、現在のタスクが終わる前に次のタスクが始まる。

エラーが発生してもタスクを繰り返し実行する問題

/**
 * (サンプル1) window.setTimeout
 */

function timeoutTask () {
  var i = 0;
  var timeId;

  function handle () {
    throw new Error(i); // Error: 0

    if (i++ < 10) {
      clearTimeout(timeId);
      timeId = setTimeout(handle, 1000);
    } 
  }

  handle();
}

timeoutTask();


/**
 * (サンプル2) window.setInterval
 */
function intervalTask () {
  var i = 0;
  var timeId;

  function handle () {
    throw new Error(i); //  Error: 0

    if (++i > 9) {
      clearInterval(timeId);
    } 
  }

  timeId = setInterval(handle, 1000);
}

intervalTask();

window.setInterval ではエラーを無限に繰り返し続けます。++i を実行する前にエラーが発生しているところに問題があります。

(サンプル2) は1秒後からタスクを実行していますが、(サンプル1) と同じように即時実行型にすることでこの問題を回避できます。

  handle();
  timeId = setInterval(handle, 1000);

handle(); で Error になるので、setInterval が実行される前にスクリプトを終了することが出来ます。

現在のタスクが終わる前に次のタスクが始まる可能性がある問題

例えば、2秒かかるタスクを1秒おきに予約すると、現在のタスクが終わる前に次のタスクが始まります。場合によっては次のタスクが終わる時刻も現在のタスクより早いかもしれません。

var i = 0;
var timeId;
var busy; // ビジー状態を表す変数

function handle () {
  if (busy) { // ビジー状態なら1秒後に試行する
    clearInterval(timeId);
    timeId = setInterval(handle, 1000);
    return;
  }

  busy = true; // ビジー状態のフラグを立てる
  // 2秒以上かかる処理

  if (++i > 9) {
    clearInterval(timeId);
  } 
}

timeId = setInterval(handle, 1000);

このようにジー状態であることを認識するようにコードを書けば回避できますが、これほど複雑なコードを書くぐらいなら window.setTimeout を採用する方が現実的だと思います。

結論

window.setInterval は実行順を保証できず、前後の処理が衝突する場合があり、無限にエラーが増殖する場合があります。
window.setTimeout と比較すると欠点ばかりでいいところが見つからなかったのが残念です…。

Google で Proxomitron が機能しないのを回避するフィルタ

フィルタ

[HTTP headers]
In = FALSE
Out = TRUE
Key = "Accept-Encoding: kill sdch (out) [2011/05/14]"
Match = "(\#,|)sdch(,\#|)"
Replace = "\@"

原因

Google Chrome は「Shared Dictionary Compression over HTTP (SDCH)」という独自の圧縮プロトコルが組み込まれており、Proxomitron はこれを解釈できません。
これはHTTPレスポンスヘッダ「Content-Encoding」を見ることで確認できます。

Content-Encoding: sdch,gzip

対策

リクエストヘッダ「Accept-Encoding」から "sdch" を削除します。そうすることで Google Webサーバはブラウザが SDCH を解釈できないと理解し、SDCH で圧縮していないデータを返します。

+++GET 779+++
GET /search?sourceid=chrome&ie=UTF-8&q=test HTTP/1.1
Host: www.google.co.jp
Accept-Encoding: gzip,deflate
...

+++RESP 779+++
HTTP/1.1 200 OK
Content-Encoding: gzip
...

ActiveXObjectとスクリプトエンジン間の循環参照によるメモリリーク

IE6 SP2- の ActiveXObject 周りのメモリリーク問題はあまり知られていないような気がしたので、簡単にまとめておきます。

jQueryprototype.js では以下の方法で回避しています。

/**
 * メモリリークを回避する方法 (jQuery および prototype.js 方式)
 */ 

function empty () {
  // この関数は何も実行せず、何も参照しない位置に置く。何も参照しない故に循環参照しなくなる。
}

function get (url) {
  var xhr = new ActiveXObject('Msxml2.XMLHTTP.6.0');

  xhr.onreadystatechange = function () { // この関数は xhr を参照可能→循環参照している
    if (xhr.readyState === 4) {
      xhr.onreadystatechange = empty;    // xhr を参照不可能な empty を代入したので循環参照しなくなる
    }
  };

  xhr.open('GET', url, false);
  xhr.send(null);
}

get('/');

上記をもう少し簡単にしたコードがこちら。

/**
 * メモリリークを回避する方法 (new Function 方式)
 */ 

function get (url) {
  var xhr = new ActiveXObject('Msxml2.XMLHTTP.6.0');

  xhr.onreadystatechange = function () {     // この関数は xhr を参照可能→循環参照している
    if (xhr.readyState === 4) {
      xhr.onreadystatechange = new Function; // Function コンストラクタによって生成されたオブジェクトは xhr を参照不可能なので循環参照しなくなる
    }
  };

  xhr.open('GET', url, false);
  xhr.send(null);
}

get('/');

jQuery 方式では function empty の位置が重要で書き方を間違えると循環参照してしまいます。

new Function 方式では空の関数をその場で生成するので、置き場所に悩まなくて済みます。

XPath の transelate() を JavaScript で実装する

概要

XPathtranslate()JavaScript で実装しました。
XPath の仕様通りに実装した translate.js と機能拡張した translate-by-array.js があります。

translate.js
XPath の仕様通り、String型の値を引数にとります。1文字単位での置換しか行えませんが、array-ver より桁違いに速く置換できます。
translate-by-array.js
配列を引数に取るバージョンです。XPath の translate 関数との互換性はなくなる代わりに2文字以上の置換を行えます。

パフォーマンス比較

jsPerf で比較テストした範囲では最速になりました。

高速化Tips

  • ECMAScript の原理上、グローバル変数が遅い(スコープチェーン)のでローカル変数にキャッシュしてます。
  • ECMAScript の原理上、プロパティアクセス演算子が遅い(プロトタイプチェーン)ので2回以上同じプロパティを参照する場合はローカル変数にキャッシュしてます。オブジェクトで辞書を作る方法も控えました。
  • 同じプロパティ参照でも new Object と new Array では new Array の方が若干速い気がします。ECMAScript 規定上は変化ないはずですが、Firefox は配列だけ特別な実装をしていたとどこかの記事で読んだ覚えが…。(array[i] と object.property で実装が異なる)
  • String#indexOf も遅くないようです。id:babu_babu_baboo さんは string.split('') で配列化して Array#indexOf していましたが、string版なら String#indexOf だけの方がコストが軽くなるんじゃないかな、と。
  • Array#push は IE7- だと遅いそうですが、最近の実装は逆なのであえて Array#push を使用しています。動作保証に IE7- を含めるなら条件付きコンパイルを使うと良いかも。
  • 置換文字列の ToString() は Array#join で処理されるので String() を省略しました。
  • 検索文字列長が置換文字列長より大きい場合に削除する方法に ToString(undefined) === '' を利用しています。Array#join 時に空文字に変換されます。

参考URL

id:babu_babu_baboo さんのブログに寄せされた情報を参考にさせていただきました。gtlt さんにはいつもお世話になってます。m(_ _)m

Twitterログ


think49
の translate() 関数。/
translate - MDC Docs
think49
図解付きでわかりやすい。/
たのしいXML: XPathXSLTの関数 translate
think49
translate() - XML Path Language (XPath)
think49
で実装してみた。/
translate.js : XPath の translate() 関数。 — Gist
think49
なるほど。string.split('') すれば処理を効率化できるんだなー。やってみよう。/
2011-05-11 - babu_babu_babooのごみ箱
think49
jsPerf でベンチマークテストしてみる。gtlt さんのコードが最速。さすがだなあ…。
think49
名前を変えて再テスト。gtlt さんのコードは Google Chrome だと爆速だなあ。よく読んでみよう。
think49
ようやく最速に出来ました。
think49
で指摘を受けた予約語について調べてみる。"char" が予約語かどうか?
think49
調べようとした矢先に gtlt さんからレスが!
たった今、"char" が の「将来の予約語」であることを確認したところです、はいw
think49
現行版では _char として予約語を回避。「translate() の文字数一致」についても調べよう。
think49
<q>For example, translate("--aaa--","abc-","ABC") returns "AAA".</q>
<cite> </cite>
think49
@ なるほど、'-' は削除するのか。
think49
i と index を間違えていた恥命的なミスを修正…。
think49
translate("--aaa--","abc-","ABC") === 'AAA'; // true
think49
検索文字列長が置換文字列長より長いとき削除するようにした。(@ 1.1.3)
think49
@ も更新。あり得ないほどに速くなっているのはなぜ?桁が違いすぎる…。
think49
@ あまり数を多くするのもどうかと思って、@ 1.1.1 のコードを @ 1.1.3 のコードに置き換えたのだけど、 の revision (URL) は変わらないのね。前回の記録が消えてしまった…。
think49
@ while で無限ループしてしまう不具合を修正。(@ 1.1.4)
translate-by-array.js を追加した。
think49
で速度をみると「version 1.1.3 >>> version 1.1.4」になるのだけど理由がさっぱりわからない…。
think49
version 1.1.3 から一部のコードを削除した版が version 1.1.4 だから version 1.1.4 の方が速いと思ってた。あり得ないぐらいに version 1.1.3 が速いけどバグ持ちだから使う気にはなれないなあ…。
azu
@ こう測るのが自然かと。さっきのrev4はtranslate_2(hankaku, hankaku, zenkaku);が呼ばれてなかったので、早くて当たり前になってた
think49
@ ありがとうございます。version 1.1.3 は [[Call]] してなかったんですね…。速いわけです。OTL
think49
改めて version 1.1.3 を削除して でテスト。
think49
array版も でテスト。候補が2つだけですが、とりあえず最速。

全角/半角文字を変換する


think49
「半角文字 <-> 全角文字」の文字マップを生成してみた。
think49
to-zenkaku.js : 半角文字を全角文字に変換する - Gist
think49
to-zenkaku.js, to-hankaku.js : 半角文字/全角文字をそれぞれ変換する — Gist
think49
しかし、円記号の半角変換は「'\uFFE5' -> '\u005C'」でいいのかな。'\u005C' はバックスラッシュのイメージがあるから違和感あるなー。
think49
U+0005C が "REVERSE SOLIDUS", U+000A5 が "YEN SIGN"
think49
円記号は U+000A5 に変換するのが妥当な模様。コードを書き換えよう。
think49
@ 「半角文字 <-> 全角文字」の文字マップを U+000A5 (YEN SIGN) に書き直した。
think49
gtlt さんからの返信。読もう。/
2011-05-10 - babu_babu_babooのごみ箱
think49
C0 Controls and Basic Latin - Unicode
(pdf), (chats-html)
think49
Halfwidth & Fullwidth Forms - Unicode
(pdf), (chats-html)

babu_babu_baboo さん作 UUID生成器 (version 5) を試してみる


think49
UUID version5 - babu_babu_babooのごみ箱
think49
@ RFC4122 に準拠したUUIDを生成するJavaScriptライブラリらしい。
think49
@ hoge({}) === hoge({}); // true
think49
@ hoge(document.getElementsByTagName('p')[0]) === hoge(document.getElementsByTagName('p')[1]); // true
think49
@ オブジェクトの UUID を生成することは出来ないのは少し残念…。

addEventListener に { handleEvent: ... } のリスナーを渡せるかは DOM Level 3 Events の規定外・実装依存です


think49
"意地悪を言えば、Object リスナを渡せるかは DOM Events の規定外・実装依存です。"
/
な、なんだってー!?(AA略)
think49
document.addEventListener('click', {msg: 'Hello', handleEvent: function () { alert(this.msg); }}, false);
think49
@ // Google Chrome 10.0.648.151, Firefox 4.0, Opera 11.01 でOK
think49
@ 手元の実装で動くので安心しきってた…。
think49
addEventListener は不正な listener を渡しても TypeError を出力しないから対策が難しい。
カスタムイベントを発火させれば { handleEvent: ... } に対応しているか、を検出できるかな。