2011年12月17日

JavaScriptの無名関数の実行 (function(){})() と (function(){}()) の違い


JavaScript Advent Calendar 2011 (オレ標準コース) 17 日目、polygon_planet です。

ずっと Advent Calendar 参加してみたいなぁと思ってたんですが
ネタが思いつかない日々で半分諦めてたんですが、考え過ぎな気がしてきたので
別におもしろい記事でもないし、技術的にも参考になるのか不明ですがとりあえず書きます。

もしかしたら同じようなこと解説してる記事がすでにあるかも…(うまく検索できてない)


JavaScript で無名関数をその場で実行するとき、
(function() {
// 処理
})();
という書き方が主流っぽいですが、
(function() {
// 処理
}());
こう書いたらどう違うのか。

ちなみに、
function() {

}(); // エラー
これは式ではなく文として扱われるためエラーです。
ただし、以下のように実行する場合はエラーではありません。
var arr = [function(){}()];        // [undefined]
var obj = {foo: function(){}()}; // {foo: undefined}

(function(){})() vs (function(){}())

どちらも同じように実行され、結果的には同じなので
とりあえず、無名関数が生成され実行するまでの JavaScript コードの過程を追ってみます。

今回は手元に SpiderMonkey に付いてる JavaScript shell しかなかったので、それを使います。
わりと前にコンパイルしたやつですが JavaScript バージョンは 1.8 なので現行と大差ないと思います。
中間コードは js shell の dis で逆アセンブルして確認します。

まず (function(){})() から。
$ js
js> dis(function() { (function(){})() });
flags: LAMBDA INTERPRETED
main:
00000: anonfunobj (function () {})
00003: group
00004: pushobj
00005: call 0
00008: pop
00009: stop

Source notes:
0: 5 [ 5] pcbase offset 5
anonfunobj というは、無名関数を定義していると思われますが、
確認のため Mozilla のソースコードを見てます。

今回は js shell のバージョンに合わせるため
/mozilla-central/ ではなく /mozilla/ を参照します。

jsopcode.tbl
305 /* Auto-clone (if needed due to re-parenting) and push an anonymous function. */
306 OPDEF(JSOP_ANONFUNOBJ, 128, "anonfunobj", NULL, 3, 0, 1, 19, JOF_OBJECT)
このあたりの JSOP_* から参照できます。
JSOP は jsopcode.c あたりで確認できます。
3777               case JSOP_ANONFUNOBJ:
3778 #if JS_HAS_GENERATOR_EXPRS
3779 sn = js_GetSrcNote(jp->script, pc);
3780 if (sn && SN_TYPE(sn) == SRC_GENEXP) {
3781 JSScript *inner, *outer;
3782 void *mark;
3783 SprintStack ss2;
3784
3785 LOAD_FUNCTION(0);
3786 inner = fun->u.i.script;
// ... (長いので省略) ...
ここでは LOAD_FUNCTION が使われています。

JavaScript バイトコードインタプリタの jsinterp.c を参照して無名関数の生成から追ってみます。
5744           BEGIN_CASE(JSOP_ANONFUNOBJ)
5745 /* Load the specified function object literal. */
5746 LOAD_FUNCTION(0);
5747
5748 /* If re-parenting, push a clone of the function object. */
5749 parent = js_GetScopeChain(cx, fp);
5750 if (!parent)
5751 goto error;
5752 obj = FUN_OBJECT(fun);
5753 if (OBJ_GET_PARENT(cx, obj) != parent) {
5754 obj = js_CloneFunctionObject(cx, fun, parent);
5755 if (!obj)
5756 goto error;
5757 }
5758 PUSH_OPND(OBJECT_TO_JSVAL(obj));
5759 END_CASE(JSOP_ANONFUNOBJ)
ここでも LOAD_FUNCTION が使われているので定義を追ってみます。
jsinterp.c
2604 #define LOAD_FUNCTION(PCOFF)                                                  \
2605 JS_GET_SCRIPT_FUNCTION(script, GET_FULL_INDEX(PCOFF), fun)
JS_GET_SCRIPT_FUNCTION を実行するマクロになっています。

じゃあ JS_GET_SCRIPT_FUNCTION はどうなってるの
jsscript.h
143 #define JS_GET_SCRIPT_FUNCTION(script, index, fun)                            \
144 JS_BEGIN_MACRO \
145 JSObject *funobj_; \
146 \
147 JS_GET_SCRIPT_OBJECT(script, index, funobj_); \
148 JS_ASSERT(HAS_FUNCTION_CLASS(funobj_)); \
149 JS_ASSERT(funobj_ == (JSObject *) STOBJ_GET_PRIVATE(funobj_)); \
150 (fun) = (JSFunction *) funobj_; \
151 JS_ASSERT(FUN_INTERPRETED(fun)); \
152 JS_END_MACRO
JSFunction にキャストして JavaScript 関数オブジェクトになることが確認できました。
(これは現行の JavaScript だともっとてっとり早くなってる)

こんな感じで追ってみると、anonfunobj は無名関数の生成ということがわかります。

次の group は、
jsopcode.tbl で同じく定義されています。
317 /* Parenthesization opcode to help the decompiler. */
318 OPDEF(JSOP_GROUP, 131, "group", NULL, 1, 0, 0, 19, JOF_BYTE)
jsopcode.c では以下のあたり。
2066               case JSOP_GROUP:
2067 cs = &js_CodeSpec[lastop];
2068 if ((cs->prec != 0 &&
2069 cs->prec <= js_CodeSpec[NEXT_OP(pc)].prec) ||
2070 pc[JSOP_GROUP_LENGTH] == JSOP_NULL ||
2071 pc[JSOP_GROUP_LENGTH] == JSOP_DUP ||
2072 pc[JSOP_GROUP_LENGTH] == JSOP_IFEQ ||
2073 pc[JSOP_GROUP_LENGTH] == JSOP_IFNE) {
// ...(中略)...
2100 }

無名関数オブジェクトを生成し、それを括弧 () によって式とし、
そしてそれを call というような流れになっています。


続いて (function(){}()) のほうも同じように。
$ js
js> dis(function() { (function(){}()) });
flags: LAMBDA INTERPRETED
main:
00000: anonfunobj (function () {})
00003: pushobj
00004: call 0
00007: pop
00008: stop

Source notes:
0: 4 [ 4] pcbase offset 4

なんと、(function(){})() と比べると
group がないだけで、ほかは同じになってしまいました。

無名関数に対する Parenthesization が発生しないので、
ちょうど、上で書いた [function(){}()] のように実行されています。

この結果から、

(function(){})() よりも、 (function(){}()) のほうが
コストが減るとわかりました。


とは言っても、どちらを使用しても実行時間が変化するとしてもマイクロ秒単位などで
JavaScript でそこまでクリティカルな処理が存在するのかも微妙なところで
あまり気にしないでいい気がします。


でも一応ベンチマークをとってみます。
js> for (var t = +new Date, i = 0, f; i < 10000000; i++) f = (function(){}());
print(+new Date - t);
10437
js> for (var t = +new Date, i = 0, f; i < 10000000; i++) f = (function(){})();
print(+new Date - t);
10485

結果:
  • (function(){}()) : 10437 ms.
  • (function(){})() : 10485 ms.

たしかに (function(){}()) のほうが速かったんですが、
10,000,000 回ループして わずか 48 ミリ秒の違い。
これを懸念してる人って…(いるのでしょうか)


そういえばこの違いについてはちょっと前に Twitter で話題(?) になって気がします。
もしかしてもうまとめられてたのかな…。だとしたら……考えたくない。


とりあえずモヤモヤした感じがなくなったのでよかった。
たまにソースコード確認してみると新鮮デスネ!


2011年12月6日

JavaScript非同期ライブラリ PotLite.js 1.23 リリース

JavaScript 非同期処理ライブラリ PotLite.js 1.23 リリースしました。

Version 1.23 は、ほとんどバグフィックスです。

AOP ぽいことができる Pot.Signal がひどくて、
シグナル解除して再度登録すると重複してコールバック関数が呼ばれちゃったりしてたので
がんばって可能な限り実行テスト増やしてフィックスして
やっと想定の動きになりました。

前バージョン (1.22) を使ってる方いたら申し訳ないです。。

PotLite.js

PotLite.js は、めんどうになりがちな非同期処理をとにかく楽にコーディングできるよう
直感的に記述できる Deferred オブジェクトを中核として実装しています。
そして、ユーザー (UI) への配慮を目的として
CPU など負荷のかからないループ処理やイテレータが利用できる JavaScript ライブラリです。


経緯などについては、以前の記事 や、 CPU 使用率のベンチマーク結果の記事 など
シグナル (イベント) については PotLite.js 1.22 リリース - アスペクト指向っぽく書けるSignal実装
などの記事を参照ください。

ダウンロード

PotLite.js 1.23

HEAD (常に最新)

レポジトリ



以下は PotLite.js について情報です。
レポジトリにも同じようなこと書いてあるので
インストールや概要など、不要な場合は読み飛ばしてください。
マニュアルのリンクは下にあります。

動作環境

以下の Web ブラウザで動作確認済みです。

  • Mozilla Firefox *
  • Internet Explorer 6+
  • Safari *
  • Opera *
  • Google Chrome *

また、以下の環境でも動作するよう設計されています。

  • Greasemonkey (UserScript)
  • Mozilla Firefox Add-On (on XUL)
  • Node.js
  • Other non-browser environment

インストール

一般的な方法で動作します。

例:

<script type="text/javascript" src="potlite.min.js"></script>
<!--または-->
<script type="text/javascript" src="http://api.polygonpla.net/js/pot/potlite/1.23/potlite.min.js"></script>

Node.js の場合。

// Example to define Pot object on Node.js.
var Pot = require('./potlite.min.js');
Pot.debug(Pot.VERSION);

Pot.Deferred.begin(function() {
    Pot.debug('Hello Deferred!');
}).then(function() {
    // ...
});
// ...

Greasemonkey (userscript) の例。

// ==UserScript==
// ...
// @require  https://github.com/polygonplanet/Pot.js/raw/master/potlite.min.js
// ...
// ==/UserScript==
Pot.Deferred.begin(function() {
    return Pot.request('http://www.example.com/data.json').then(function(res) {
        return Pot.parseFromJSON(res.responseText);
    });
}).then(function(res) {
    Pot.debug(res);
    // do something...
});
//...
PotLite.js をバージョンを限定して Web から直接読み込みたい場合、
上の GitHub リンクでは常に最新になってしまうため
実装の差異による不具合が発生するかもしれません。
そのため、ホスティング用 API サーバを用意しています。
これは、1.23 の部分をリリース済みのバージョンに合わせて変更できます。
レポジトリに (例えば 1.xx と) バージョンをタグ付けした時に、
あわせて API サーバに置くようにしています。

例えば Greasemonkey で 1.23 を使いたい場合、
// ==UserScript==
// ...
// @require  http://api.polygonpla.net/js/pot/potlite/1.23/potlite.min.js
// ...
// ==/UserScript==
と記述できます。

Greasemonkey に限らず script タグからでもなんでも自由に使ってください。


jQuery プラグインとしての例:

// jQuery を読み込んだ後に実行。
Pot.deferrizejQueryAjax();

// Ajax 系の関数が Pot.Deferred を返すようになる
$.getJSON('/hoge.json').then(function(data) {
    alert(data.results[0].text);
}).rescue(function(err) {
    alert('Error! ' + err);
}).ensure(function() {
    return someNextProcess();
});

// エフェクトなどを Deferred 化する 'deferred' が追加される
$('div#hoge').deferred('hide', 'slow').then(function() {
    // .hide() が終了したあとの処理
});

Pot.deferrizejQueryAjax() は v1.23 現在、ライブラリ側からは自動で実行しません。
なので、プラグインとして利用する場合は コードの最初などで一度コールしてください。

リファレンス・マニュアル

より詳しい情報はすべてリファレンスに載っています。
基本的な導入や、各メソッド・関数についても扱っています。

動作テスト

以下のページで動作テストができます。
ページを開くと実装されている主な関数・メソッドを全てテストします。

自動生成されたドキュメント

Closure Compiler によりソースコードから自動生成されたドキュメントです。

殆どの関数ごとにサンプルコードを載せているので、ある程度は参考になると思います。
生成物をすべて確認しているわけではないので、誤認識してる箇所もあるかもしれません。
より詳細な実装などは直接ソースコードを参照ください。




不明な点、要望やバグや感想などありましたら
@polygon_planet や 下のレポジトリから、またはメールでなんでも伝えてください。

レポジトリ

2011年12月5日

マウス下の画像をGoogle画像検索するTomblooパッチ

Firefox アドオン Tombloo のパッチです。

機能概要

マウス下の画像を Google 画像検索 (Google Image Search) で検索できるようになります。
Taberareloo には既に付いていて、せっかくなので作りました。
あと、二次画像詳細検索 も選べます。

コンテキストメニューの 「Tombloo」 から操作できます。


今のところメニューは 2 つだけですが、
なにか思いついたら追加して随時アップデートする予定です。

パッチ (Download or Update):

インストール/アップデート:

上のパッチリンクを
右クリック→「Tombloo」→「Tomblooパッチのインストール」でインストール/アップデート。


※直接右クリックからインストールできない場合:

できない場合は

「Tomblooパッチのインストールに失敗しなくなるパッチ」 のパッチ/記事をインストールもしくは参照ください。

レポジトリ

2011年11月23日

JavaScript非同期ライブラリ PotLite.js 1.22 リリース。アスペクト指向っぽく書けるSignal実装

追記

PotLite.js 1.22 はバギーなため 記事内のリンクを 1.23 に変更しました。

PotLite.js 1.22 リリースしました。

PotLite.js は、非同期処理や負荷のかからないループ処理やイテレータを重視した JavaScript ライブラリです。
経緯などについては、以前の記事 や、 CPU 使用率のベンチマーク結果の記事 を参照ください。

ダウンロード

PotLite.js 最新

レポジトリ


PotLite.js バージョン 1.22 では Pot.Signal オブジェクトを実装しました。

Pot.Signal は、任意のハンドラを登録し シグナルを送信して実行に移すことが可能です。
言い方を変えると、自らイベントを作成し、任意のタイミングで呼び出せるようなものです。

「イベント」という概念で実装したわけではありませんが、ここでは便宜上イベントという言い方をします。

Pot.Signal には大まかに以下の関数があります。

  • attach : イベントを登録
  • detach : イベントを解除
  • signal : イベントを実行

Pot.Signal の使用例

Pot.Signal はいろいろな使い方があります。
例えば、
// イベントを登録
//  (第一引数に文字列を渡すと document.getElementById を使用する)
var handler = attach('#foo', 'click', function(ev) {...});

// イベント解除
detach(handler);
上のように addEventListener/removeEventListener の代わりに使用したり、
var MyObj = {};

// 独自のシグナルを登録
var handler = attach(MyObj, 'clear-data', function() {
  // プロパティの初期化など
  MyObj.data = null;
  MyObj.time = null;
});

attach(window, 'load', function() {
  // 初期化するシグナルを送信
  signal(MyObj, 'clear-data');

  // リセットボタンを押した時にもクリアするよう設定
  attach('#reset', 'click', function() {
    signal(MyObj, 'clear-data');
  });

  // 既存の処理など
  myLoadProcess();
  //...
});
上のように attach, signal を独自のシグナルとして使用したりできます。

そして、attachBefore と attachAfter を使うと処理の前後に何らかの動作を追加できます。
// 保存ボタンを押した時のイベントを設定
attach('#saveData', 'click', function() {
  // 保存する関数
  saveData(document.getElementById('inputText').value);
  // ユーザーに保存を伝える関数
  showSaveData('Saved!');
});

// これにフォーカス移動するよう後付けする
attachAfter('#saveData', 'click', function() {
  document.getElementById('inputText').focus();
});

// その前にログを取るよう設定する
attachBefore('#saveData', 'click', function() {
  MyLogger.log('Save inputText');
});
attachBefore, attachAfter はいくつでも登録できます。
登録した順に実行されます。

また、attachPropBefore, attachPropAfter という関数もあります。
これは、attachBefore, attachAfter と違い、
オブジェクトの関数にダイレクトに作用します。
オブジェクトの持つ関数が呼ばれると、自動的に Before, After のハンドラ関数が実行されます。


例えば何らかのアプリケーションを実行する時に、ログをとりたくなった場合。
var MyApp = {
  execute : function() {
    // 何らかの処理を開始する
    myAppDoit();
  }
  // ...
};

attach('#execute', 'click', function() {
  // アプリケーションを実行
  MyApp.execute();
});

// 実行する前にログを取る
attachPropBefore(MyApp, 'execute', function() {
  MyLogger.log('Begin execute');
});

// 実行した後のログを取る
attachPropAfter(MyApp, 'execute', function() {
  MyLogger.log('End execute');
});
このように設定することで、上の例の場合では
MyApp.execute が実行される前後に任意の処理を追加できます。

解除したくなったら、
var handler = attach(...);

// シグナルを解除
detach(handler);
attach*() 関数の戻り値のハンドラオブジェクトを使い、
detach (Pot.Signal.detach) で解除できます。

他にも detachAll() を使い一括で解除することもできます。


Pot.attach(), Pot.signal() などは Pot.Signal.attach() として実装されていますが、
Pot オブジェクトからも参照できます。
Pot.globalize() を実行しておくと、単に attach(obj, ...) と書けます。
今回の例では attach と表現しています。


これらの関数、とくに Before, After を使うことで、
アスペクト指向 (AOP) っぽいプログラミングが可能となります。

Pot.Signal は Node.js などのサーバでの利用も可能ですが、
Web ブラウザ上、HTML5 での Web Storage (localStorage や sessionStorage) を使う時などに便利と思います。

また、attach 系の関数 (attach*) の後に .once を付けると、一度だけ実行されます。
// 一度クリックしたら解除される
attach.once('#hoge', 'click', function() {...});

Pot.Signal の関数

Pot.Signal.attach(object, signalName, callback[, useCapture])
スロットにシグナルを登録します。 Pot.Signal が保持しているハンドラリストに任意のオブジェクトとコールバック関数を登録します。 引数 object が DOM エレメントだった場合、addEventListener と同じように働きます。 戻り値は、Pot.Signal.Handler のインスタンスです。
Pot.Signal.attachBefore(object, signalName, callback[, useCapture])
スロットに存在するハンドラが呼ばれる前に実行されるシグナルを登録します。 Pot.Signal.attach() で登録したものと同じシグナル signalName で登録した場合、 attach() のハンドラが呼ばれる前に実行されます。 同じシグナルが存在しない場合 signal() によって実行しても何も起きません。 DOM エレメントに対して attach() した場合も同様です。 戻り値は、Pot.Signal.Handler のインスタンスです。
Pot.Signal.attachAfter(object, signalName, callback[, useCapture])
スロットに存在するハンドラが呼ばれた後に実行されるシグナルを登録します。 Pot.Signal.attach() で登録したものと同じシグナル signalName で登録した場合、 attach() のハンドラが呼ばれた後に実行されます。 同じシグナルが存在しない場合 signal() によって実行しても何も起きません。 DOM エレメントに対して attach() した場合も同様です。 戻り値は、Pot.Signal.Handler のインスタンスです。
Pot.Signal.attachPropBefore(object, propName, callback)
object が持つ関数 propName が呼ばれる前に実行されるシグナル callback を登録します。 attachBefore() と違い、Pot.Signal.signal() を呼ばなくても object[propName] が呼ばれた時に自動的に実行されます。 attachPropBefore は、DOM オブジェクトに対しての動作は保証されません。 戻り値は、Pot.Signal.Handler のインスタンスです。
Pot.Signal.attachPropAfter(object, propName, callback)
object が持つ関数 propName が呼ばれた後に実行されるシグナル callback を登録します。 attachAfter() と違い、Pot.Signal.signal() を呼ばなくても object[propName] が呼ばれた時に自動的に実行されます。 attachPropAfter は、DOM オブジェクトに対しての動作は保証されません。 戻り値は、Pot.Signal.Handler のインスタンスです。
Pot.Signal.detach(object[, signalName[, callback[, useCapture]]])
Pot.Signal.attach*() によって登録したシグナルを 1 つ解除します。 attachBefore, attachAfter, attachPropBefore, attachPropAfter で登録したものも同様に解除可能です。 引数に object だけを渡した場合、関連するシグナルが 1 つ解除されます。 引数 signalName, callback を渡すと明確に解除できます。 また、attach() などの戻り値のハンドラを object として渡して解除もできます。 登録と解除の関係は、setTimeout と clearTimeout の関係と同様に扱えます。 DOM エレメントに対しては、 removeEventListener と同様に扱うことができます。 解除成功時に true、失敗時に false が返ります。
Pot.Signal.detachAll([object[, ...signals]])
Pot.Signal.attach*() によって登録したシグナルをすべて解除します。 attachBefore, attachAfter, attachPropBefore, attachPropAfter で登録したものも同様に解除可能です。 引数に object だけを渡した場合、関連するシグナルがすべて解除されます。 引数 signals を渡すと一致するシグナルがすべて解除できます。 シグナル名となる signals は文字列または配列で複数指定できます。
Pot.Signal.signal(object, signalName[, ...args])
Pot.Signal.attach() によって登録したシグナルを実行します。 引数 object と signalName に一致するシグナルがすべて実行されます。 実行される関数には第三引数~以降に任意の引数が渡せます。

PotLite.js

以下は PotLite.js について情報です。
インストールや概要など、不要な場合は読み飛ばしてください。

PotLite.js ダウンロード

最新

レポジトリ

$ git clone git://github.com/polygonplanet/Pot.js

GitHub : polygonplanet/Pot.js

動作環境

以下の Web ブラウザで動作確認済みです。

  • Mozilla Firefox *
  • Internet Explorer 6+
  • Safari *
  • Opera *
  • Google Chrome *

また、以下の環境でも動作するよう設計されています。

  • Greasemonkey (userscript)
  • Mozilla Firefox Add-On (on XUL)
  • Node.js
  • Other non-browser environment

インストール

一般的な方法で動作します。

例:

<script type="text/javascript" src="potlite.min.js"></script>
<!--または-->
<script type="text/javascript" src="http://api.polygonpla.net/js/pot/potlite/1.23/potlite.min.js"></script>

Node.js の場合。

// Example to define Pot object on Node.js.
var Pot = require('./potlite.min.js');
Pot.debug(Pot.VERSION);

Pot.Deferred.begin(function() {
  Pot.debug('Hello Deferred!');
}).then(function() {
  // ...
});
// ...

Greasemonkey (userscript) の例。

// ==UserScript==
// ...
// @require  https://github.com/polygonplanet/Pot.js/raw/master/potlite.min.js
// ...
// ==/UserScript==
Pot.Deferred.begin(function() {
  return Pot.request('http://www.example.com/data.json').then(function(res) {
    return Pot.parseFromJSON(res.responseText);
  });
}).then(function(res) {
  Pot.debug(res);
  // do something...
});
//...
PotLite.js をバージョンを限定して Web から直接読み込みたい場合、
上の GitHub リンクでは常に最新になってしまうため
実装の差異による不具合が発生するかもしれません。
そのため、API サーバを用意しました。
これは、1.23 の部分をリリース済みのバージョンに合わせて変更できます。
レポジトリに (例えば 1.xx と) バージョンをタグ付けした時に、
あわせて API サーバに置くようにしています。

例えば Greasemonkey で 1.23 を使いたい場合、
// ==UserScript==
// ...
// @require  http://api.polygonpla.net/js/pot/potlite/1.23/potlite.min.js
// ...
// ==/UserScript==
と記述できます。

Greasemonkey に限らず script タグからでもなんでも自由に使ってください。



jQuery プラグインとしての例:

// jQuery を読み込んだ後に実行。
Pot.deferrizejQueryAjax();

// Ajax 系の関数が Pot.Deferred を返すようになる
$.getJSON('/hoge.json').then(function(data) {
  alert(data.results[0].text);
}).rescue(function(err) {
  alert('Error! ' + err);
}).ensure(function() {
  return someNextProcess();
});

// エフェクトなどを Deferred 化する 'deferred' が追加される
$('div#hoge').deferred('hide', 'slow').then(function() {
  // ( hide() が終了したあとの処理)
});

Pot.deferrizejQueryAjax() は現状、
ライブラリ側で実行しません。
なので、プラグインを使用する場合は
コードの最初などで実行する必要があります。

リファレンス・マニュアル

より詳しい情報はすべてリファレンスに載っています。
基本的な導入や、各メソッド・関数についても扱っています。

動作テスト

以下のページで動作テストができます。
ページを開くと実装されている主な関数・メソッドを全てテストします。

自動生成されたドキュメント

Closure Compiler によりソースコードから自動生成されたドキュメントです。

殆どの関数ごとにサンプルコードを載せているので、ある程度は参考になると思います。
生成物をすべて確認しているわけではないので、誤認識してる箇所もあるかもしれません。
より詳細な実装などは直接ソースコードを参照ください。




不明な点、要望やバグや感想などありましたら
@polygon_planet や 下のレポジトリから、またはメールでなんでもどぞです。

レポジトリ

2011年11月5日

フリーズやカクカクしない安定した動作のアプリやアドオン、ゲームなどが作れるJavaScriptライブラリ

Web を閲覧していると、突然 CPU 100% になったり
定期的に負荷がかかったりします。

flash などを除くと、それ以外のほとんどは JavaScript による重いループ処理が原因です。
少しずつ改善されてるような気はしますが
おそらくもう何年も前から、この問題がついてまわっていたのではないでしょうか。

そしてこのような負荷は、高スペックなマシンでは気付かないと思います。
開発者やテスターが高スペックな PC で動作確認を行ったことにより、
瞬間的な負荷を見過ごされたまま公開、リリースされてしまうこともあると思います。
それを、低スペックなマシンや iPhone などスマホを使っているユーザーが実行し
負荷に耐え切れずカクカクな描画になってしまったり、
最悪フリーズしてしまう場合もあります。

マシンの性能に限らず、例え高スペックマシンでも
いくつものアプリケーションを常時起動していて
タスクがいっぱいになっているユーザーもこのような事例に該当します。

何らかのアプリケーションを開発する場合、
処理の高速化は常に求められます。
より高速に動作したほうが軽快なのは確かです。

ですがクライアントアプリケーションを作る場合、
CPU 100% 使う処理を 10 秒も 20 秒も続けられてしまったら
それはもう不快でしかありません。
さらにそれが非ブロックでなく実行されてたら
マシンはもうフリーズのような動かない状態になりっぱなしです。

単純に高速を目指すのではなく、UI のことを考え制御を返すことが重要です。
UI を無視してひたすら高速に実行して 5 秒かかる処理だったら、
定期的に UI に制御を返し
例え数秒遅くなってもバックグラウンドで行い CPU 負荷を抑えることが
ユーザーへのストレス軽減として重要と考えています。

さらに非同期で実行することで並列化も可能になり、
逆に本来より速く実行できる可能性もあります。


PotLite.js は、そのような負荷軽減を可能とする JavaScript ライブラリです。
非同期処理をチェインで扱う Deferred オブジェクトを中心に
いろいろなループ処理を非同期で実行できるイテレータを実装しています。

イテレータは PotLite.js バージョン 1.21 現在、
  • forEach
  • repeat
  • forEver
  • iterate
  • items
  • zip
  • map
  • filter
  • reduce
  • every
  • some
が実装されています。
非同期での実行、同期での実行、そして Deferred チェイン上での実行が可能です。

そして、速度指定できるのが特徴の一つです。
Deferred.forEach(obj, function(value, key) {...})
上のコードが forEach (jQuery での jQuery.each のようなもの) の非同期での通常実行になり、
Deferred.forEach.slow(obj, function(value, key) {...})
上のように .slow と速度を明示することができます。
すると各ループをゆっくり目で実行します。

他にも .fast などの指定ができます。
関数内で return wait(1); 等と Pot.Deferred オブジェクトを返すことで
各ループ間でより細かな wait などの調整ができます。
逆に、非同期イテレートをより速く実行したい場合は
Pot.Defered.forEach.ninja(...) と、
最も速く実行するよう定義されている .ninja を指定すると
通常の for 文を単に関数で包んだのと同じくらいの速度で実行できます。

より詳しくは リファレンス を参照してください。


どれくらい変化があるかベンチマークをとってみました。
イテレータ JSON ファイルサイズ CPU 最大使用率 CPU 平均使用率 実行時間 (ms)
for 文 1MB 14.52% 10.61% 1211ms.
jQuery.each 1MB 16.28% 13.53% 1362ms.
Pot.Deferred.forEach 1MB 11.65% 5.53% 1647ms.
for 文 5MB 24.85% 21.71% 4717ms.
jQuery.each 5MB 27.78% 24.29% 5570ms.
Pot.Deferred.forEach 5MB 13.74% 8.61% 8925ms.
for 文 10MB 28.14% 25.92% 10699ms.
jQuery.each 10MB 31.09% 30.77% 15844ms.
Pot.Deferred.forEach 10MB 18.62% 9.77% 38909ms.

同期と非同期の根本的な違いがありますが
この結果は、下記のネストループを Core i3 で 5 回実行した平均結果です。
JSON ファイルは、はてなキーワード一覧 から取得した CSV をキーワードだけの JSON に変換したものです。
10MB はキーワードすべて、5MB は半分で切った JSON ファイル、 1MB はそのまた半分です。

Pot.Deferred.forEach によるイテレータは、処理時間が他と比べ長くなっていますが
CPU 最大使用率、CPU 平均使用率 共に安定しているのがわかります。


CPU グラフも比較してみました。
すべてのキーワードを対象に、Pot.Deferred.forEach (上) と、jQuery.each (下) をそれぞれ実行した結果です。

jQuery.each は、途中で「警告:応答しないスクリプト」のダイアログがでてしまいました。
そのため、本来なら Pot.Deferred.forEach のほうが時間がかかると予想してください。

それでも、Pot.Deferred.forEach では jQuery.each に比べ
瞬間的 (もしくは断続的) な負荷を抑えることができています。
これは、対象のデータがどれほど巨大でも
負荷のかからないイテレートが可能なことを示しています。


なお、上記のベンチマークは
ある程度負荷のかかるループ処理 を対象としています。
PotLite.js のイテレータは、各ループがある程度重いと判断すると CPU 負荷を抑えるよう働きます。
つまり、逆に言うと
瞬時に終わるような小中規模のループでは、他のループと変わらない速度で実行されるということです。

以下に、小規模のループ結果を記します。
イテレータCPU 最大使用率CPU 平均使用率実行時間 (ms)
for 文4.10%3.87%9ms.
jQuery.each5.54%4.72%18ms.
Pot.Deferred.forEach4.71%4.15%19ms.
この結果は、1MB の JSON ファイルを対象に ユニーク処理をなくした実行結果です。
上と同じく 5 回実行した結果の平均になります。

PotLite.js は、jQuery.each とほぼ同じ実行速度で、
ちょうど for 文を関数で包んだものを実行したのと同じくらいの速度で実行できています。

このような毎回の処理が小さなループでは、 for 文などが最も適していますが
適度に各関数を使うことでコストも減少させることができます。

以下は、テストの使用したソースコードです。
// jQuery.each
function benchmark_jQuery() {
  $.getJSON('hatena.keywords.json?callback=?', {
    size : '10mb'
  }, function(data) {
    var results = [];
    var start = +new Date;
    $.each(data.keywords, function(k, word) {
      if (!/[^a-zA-Z0-9_.-]/.test(word)) {
        var uniq = true;
        for (var i = 0; i < results.length; i++) {
          if (word === results[i]) {
            uniq = false;
            break;
          }
        }
        if (uniq) {
          results.push(word);
        }
      }
    });
    $('#result').text((+new Date) - start);
  });
}
巨大なファイルの転送と負荷のテストのため、ソースのみ記します。
// Pot.Deferred.forEach
function benchmark_potlite() {
  begin(function() {
    return jsonp('hatena.keywords.json?callback=?', {
      queryString : {
        size : '10mb'
      }
    }).then(function(res) {
      var results = [];
      var start = now();
      return Deferred.forEach(res.keywords, function(word) {
        if (!/[^a-zA-Z0-9_.-]/.test(word)) {
          var uniq = true;
          for (var i = 0; i < results.length; i++) {
            if (word === results[i]) {
              uniq = false;
              break;
            }
          }
          if (uniq) {
            results.push(word);
          }
        }
      }).then(function() {
        return now() - start;
      });
    }).then(function(time) {
      $('#result').text(time);
    });
  });
}
以下は、小規模のループとしてテストしたソースコードです。
// jQuery.each
function benchmark_jQuery_lite() {
  $.getJSON('hatena.keywords.json?callback=?', {
    size : '1mb'
  }, function(data) {
    var results = [];
    var start = +new Date;
    $.each(data.keywords, function(k, word) {
      if (!/[^a-zA-Z0-9_.-]/.test(word)) {
        results.push(word);
      }
    });
    $('#result').text((+new Date) - start);
  });
}
// Pot.Deferred.forEach
function benchmark_potlite_lite() {
  begin(function() {
    return jsonp('hatena.keywords.json?callback=?', {
      queryString : {
        size : '1mb'
      }
    }).then(function(res) {
      var results = [];
      var start = now();
      return Deferred.forEach(res.keywords, function(word) {
        if (!/[^a-zA-Z0-9_.-]/.test(word)) {
          results.push(word);
        }
      }).then(function() {
        return now() - start;
      });
    }).then(function(time) {
      $('#result').text(time);
    });
  });
}

PotLite.js

PotLite.js は、Pot.js の軽量バージョンです。 非同期処理のオブジェクト/関数だけに絞ったライブラリです。 経緯などは Pot.js に関する以前の記事 を参照ください。

概要

PotLite.js は、非ブロックでの非同期処理を直列的に書けるようにし、 UI や CPU への負担を軽減するループ処理を中心に実装された JavaScript ライブラリです。 MochiKit ライクな Deferred オブジェクトにより 様々なイテレート (forEach, filter, map, repeat, some など) を可能とします。 ※ここでいう MochiKit ライクとは、JSDeferred とは違い 1 つのチェインが 1 つのインスタンスということです。 ※Deferred チェイン は JSDeferred や MochiKit.Async.Deferred と同じ感覚で扱えます。

ダウンロード

最新

レポジトリ

$ git clone git://github.com/polygonplanet/Pot.js
GitHub : polygonplanet/Pot.js

動作環境

以下の Web ブラウザで動作確認済みです。
  • Mozilla Firefox *
  • Internet Explorer 6+
  • Safari *
  • Opera *
  • Google Chrome *
また、以下の環境でも動作するよう設計されています。
  • Greasemonkey (userscript)
  • Mozilla Firefox Add-On (on XUL)
  • Node.js
  • Other non-browser environment

インストール

一般的な方法で動作します。
例:
<script src="potlite.min.js" type="text/javascript">
</script>
Node.js の場合。
// Example to define Pot object on Node.js.
var Pot = require('./potlite.min.js');
Pot.debug(Pot.VERSION);

Pot.Deferred.begin(function() {
  Pot.debug('Hello Deferred!');
}).then(function() {
  // ...
});
// ...
Greasemonkey (userscript) の例。
// ==UserScript==
// ...
// @require  https://github.com/polygonplanet/Pot.js/raw/master/potlite.min.js
// ...
// ==/UserScript==
Pot.Deferred.begin(function() {
  return Pot.request('http://www.example.com/data.json').then(function(res) {
    return Pot.parseFromJSON(res.responseText);
  });
}).then(function(res) {
  Pot.debug(res);
  // do something...
});
//...
PotLite.js をバージョンを限定して Web から直接読み込みたい場合、 上の GitHub リンクでは常に最新になってしまうため 実装の差異による不具合が発生するかもしれません。 そのため、API サーバを用意しました。
これは、1.21 の部分をリリース済みのバージョンに合わせて変更できます。 レポジトリに (例えば 1.22 と) バージョンをタグ付けした時に、 あわせて API サーバに置くようにしています。 例えば Greasemonkey で 1.21 を使いたい場合、
// ==UserScript==
// ...
// @require  http://api.polygonpla.net/js/pot/potlite/1.21/potlite.min.js
// ...
// ==/UserScript==
と記述できます。 Greasemonkey に限らず script タグからでもなんでも自由に使ってください。 ----
jQuery プラグインとしての例:
// jQuery を読み込んだ後に実行。
Pot.deferrizejQueryAjax();

// Ajax 系の関数が Pot.Deferred を返すようになる
$.getJSON('/hoge.json').then(function(data) {
  alert(data.results[0].text);
}).rescue(function(err) {
  alert('Error! ' + err);
}).ensure(function() {
  return someNextProcess();
});

// エフェクトなどを Deferred 化する 'deferred' が追加される
$('div#hoge').deferred('hide', 'slow').then(function() {
  // ( hide() が終了したあとの処理)
});
Pot.deferrizejQueryAjax() は現状、 ライブラリ側で実行しません。 なので、プラグインを使用する場合は コードの最初などで実行する必要があります。

リファレンス・マニュアル

より詳しい情報はすべてリファレンスに載っています。 基本的な導入や、各メソッド・関数についても扱っています。

動作テスト

以下のページで動作テストができます。 ページを開くと実装されている主な関数・メソッドを全てテストします。

自動生成されたドキュメント

Closure Compiler によりソースコードから自動生成されたドキュメントです。
殆どの関数ごとにサンプルコードを載せているので、ある程度は参考になると思います。 生成物をすべて確認しているわけではないので、誤認識してる箇所もあるかもしれません。 より詳細な実装などは直接ソースコードを参照ください。

久しぶりにちょっとがんばって重点をまとめてみました。 これを機に、Pot.js (PotLite.js) で遊んでくれると嬉しいです。 不明な点、要望やバグや感想などありましたら @polygon_planet や 下のレポジトリから、またはメールでなんでもどぞです。

レポジトリ

2011年11月3日

ページ内のDOM要素を削除したり操作できるTomblooパッチ

Firefox アドオン Tombloo のパッチです。

機能概要

ページ内の DOM 要素を削除したりします。
主にウザい要素を消し去るのに便利かも。
マウス下の HTML エレメントや、選択範囲の要素を削除できます。
Ctrl+A (全選択) して選択範囲を消すと何もなくなり寂しい気分が味わえます。

こんなことは Firebug で事足りるのですが、
パネルを出したり戻したりするのすら めんどくさいので作りました。

Adblock など常時使ってる方にはあまり意味ないかもしれません。

コンテキストメニューの 「Tombloo」 から操作できます。

今のところメニューは 2 つだけですが、
なにか思いついたら追加して随時アップデートする予定です。

パッチ (Download or Update):

インストール/アップデート:

上のパッチリンクを
右クリック→「Tombloo」→「Tomblooパッチのインストール」でインストール/アップデート。


※直接右クリックからインストールできない場合:

できない場合は

「Tomblooパッチのインストールに失敗しなくなるパッチ」 のパッチ/記事をインストールもしくは参照ください。

レポジトリ

2011年11月2日

新GoogleReaderで左サイドバー非表示ボタンをつけるGreasemonkey

こないだから新しいインターフェイスになった Google リーダーにまだ慣れない。
f 押すとフルスクリーンになることを知ったのはいいのですが
マウス操作に慣れてたのでどうしようかと。
そのためにジェスチャー拡張したりマウスボタン拡張したりとかしたくなかったから、
とりあえず Greasemonkey で書きました。
左サイドバーをトグル表示 (表示/非表示) するボタンがつくだけのシンプルなスクリプトです。

Download