JavaScript
Up one levelGoogle Closureを使ってみるかも (1)
はじめまして。アリエルでプログラマしてる内田と申します。
http://blog.pasonatech.co.jp/ootani/
「JavaScriptのコードを書き直せ」「ただし使うライブラリはjQueryとClosureの2択で」と指令を受け紆余曲折あった結果、私はClosure派になりました。
とりあえずjQueryはコアのコードがとても読みづらいのです。
http://code.jquery.com/jquery-latest.js
英語を読めない私にとってコードの可読性は死活問題です。
Closure Libraryならコード自体が読みやすいしAPIリファレンスもしっかりしている。
http://closure-library.googlecode.com/svn/trunk/closure/goog/docs/index.html
リファレンスにソースコードやデモへのリンクが貼ってあって便利です。
なので私としてはClosure Libraryでさっさと書き直したいのですが、jQuery派のえらい人に
「ダイアログのリサイズができないなんてダメダメじゃないか」と言われてしまいました。
http://closure-library.googlecode.com/svn/trunk/closure/goog/demos/dialog.html
本当だ。困った。どうしよう。
http://closure-library.googlecode.com/svn/trunk/closure/goog/docs/closure_goog_ui_dialog.js.source.html
↑goog.ui.dialog内のonResizeはブラウザのリサイズイベントを監視してるだけ。
http://closure-library.googlecode.com/svn/trunk/closure/goog/docs/class_goog_dom_ViewportSizeMonitor.html
↑こいつも同じ。
http://closure-library.googlecode.com/svn/trunk/closure/goog/docs/class_goog_fx_dom_Resize.html
↑こいつはリサイズのアニメーション効果用。
http://closure-library.googlecode.com/svn/trunk/closure/goog/docs/class_goog_ui_SplitPane.html
↑一番近いことやっているのがSplitPaneだけど、これ以外に「ドラッグ操作でサイズ変更する」ようなことしているものがなさそうでした。
(ちなみにSplitPaneのデモでJavaScriptエラーが出てます。FireBug見ればすぐわかると思いますが、「goog.require('goog.ui.Dom');」が原因なのでコメントアウトすれば動きます。本当は「goog.require('goog.dom');」と書かなければいけなそう。)
jQueryと併用できるかな?
- Category(s)
- JavaScript
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/uchida/google-closure30924f7f30633066307f308b304b3082/tbping
closure libraryのBrowserEventが微妙
以前closure libraryのdialogがリサイズできないと書きました。
しかたがないのでgoog.fx.Draggerを使ってExt.jsやjQuery UIのResizableクラスを作ってリサイズできるようにしました。
しかし動かしているとなんとなく元のダイアログより遅く感じます。
ちなみに元のダイアログはscript.aculo.usのdraggableで作ったアリエル製リサイズクラスを使ってます。
最初はドラッグ中にリアルタイムでリサイズしているせい(Ext.Resizablelのdynamic=trueやjQuery UIのghost=falseの状態)だと思い、ドロップ時だけリサイズするためのプロパティを追加したのですがそれでもまだ少しだけカクカクします。
私の環境ではほとんど気にならないレベルですが古いPCだとどうなるのかわかりません。
ちょっと怖いので、Fire BugでプロファイルをとってみるとBrowserEventのinitイベントがトップになります。
http://closure-library.googlecode.com/svn/trunk/closure/goog/docs/closure_goog_events_browserevent.js.source.html
goog.events.BrowserEvent.prototype.init = function(e, opt_currentTarget) {さらに調べるためこれを
//(中略)
this.offsetX = e.offsetX !== undefined ? e.offsetX : e.layerX;
this.offsetY = e.offsetY !== undefined ? e.offsetY : e.layerY;
this.clientX = e.clientX !== undefined ? e.clientX : e.pageX;
this.clientY = e.clientY !== undefined ? e.clientY : e.pageY;
this.screenX = e.screenX || 0;
this.screenY = e.screenY || 0;
this.button = e.button;
this.keyCode = e.keyCode || 0;
this.charCode = e.charCode || (type == 'keypress' ? e.keyCode : 0);
this.ctrlKey = e.ctrlKey;
this.altKey = e.altKey;
this.shiftKey = e.shiftKey;
this.metaKey = e.metaKey;
this.event_ = e;
delete this.returnValue_;
delete this.propagationStopped_;
};
goog.events.BrowserEvent.prototype.init = function(e, opt_currentTarget) {のように書き直して(関数内関数最高!)調べていくとどうも
//(中略)
function f1() {
this.offsetX = e.offsetX !== undefined ? e.offsetX : e.layerX;
this.offsetY = e.offsetY !== undefined ? e.offsetY : e.layerY;
}
function f2() {
this.clientX = e.clientX !== undefined ? e.clientX : e.pageX;
this.clientY = e.clientY !== undefined ? e.clientY : e.pageY;
}
function f3() {
this.screenX = e.screenX || 0;
this.screenY = e.screenY || 0;
}
f1.call(this);
f2.call(this);
f3.call(this);
this.button = e.button;
this.keyCode = e.keyCode || 0;
this.charCode = e.charCode || (type == 'keypress' ? e.keyCode : 0);
this.ctrlKey = e.ctrlKey;
this.altKey = e.altKey;
this.shiftKey = e.shiftKey;
this.metaKey = e.metaKey;
this.event_ = e;
delete this.returnValue_;
delete this.propagationStopped_;
};
this.screenX = e.screenX || 0;の2行が全体の80%くらい時間がかかっている事が判明しました。
this.screenY = e.screenY || 0;
script.aculo.us版ではscreenX、screenYは使っていなかったのでこれが遅さの原因でしょう。
話がずれますが、Fire Bug1.3から1.4になったときに
hoge: function() {だとスタックトレース時に無名関数扱いになったんですよね。デバッグし辛くなって困るんですが。
ところがプロファイル時は名前がちゃんと取れてました。ふっしぎー。
goog.events.BrowserEventは名前のとおりブラウザのイベントをラップするクラスなので、closure libraryのイベント管理を使ってブラウザのイベントを監視する場合必ず生成されるはずです。
IEではどうなのかわかりませんが、FireFoxだと動きを見る限りscreenX、screenYの評価を遅延することで速度をあげようとしています。
なのにclosure libraryのイベント管理を使うとscreenX、screenYを使うか使わないかに関わらず必ずscreenX、screenYが計算されてしまいます。
これって微妙じゃないですか?
ついでにFireFoxのソースも確認してみました。
http://hg.mozilla.org/tracemonkey/file/10dc623ee90f/content/events/src/nsDOMUIEvent.cpp
↑のGetScreenPointメソッドと
http://hg.mozilla.org/tracemonkey/file/10dc623ee90f/content/events/src/nsDOMMouseEvent.cpp
↑のGetScreenX、GetScreenYメソッドだと思います。
計算結果のキャッシュとかしてないのかな?
だとしたらGetScreenX、GetScreenYで全く同じ計算を2回やってることになりそうだけど。嫌だなあ。
せめてJavaScriptからGetScreenPointを直接叩けるといいんだけど。
- Category(s)
- JavaScript
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/uchida/closure-library306ebrowserevent5fae5999/tbping
Re:closure libraryのBrowserEventが微妙
私も、最近 Closure Library を使っていて、同じようなことを思ったんですが、
きっと Closure Library って Closure Compiler とセットで使うことを主眼に置いてるんじゃないでしょうか。
Closure Compiler を使って強い最適化をすれば、おそらく「使わないプロパティへの値の設定」は省略されると思います。
まあ Closure Compiler を使わないとパフォーマンスが出ないっていうのも微妙だとは思いますが。
Re:closure libraryのBrowserEventが微妙
いつもブログの方拝見させてもらっています。
> Closure Compiler を使って強い最適化をすれば、おそらく「使わないプロパティへの値の設定」は省略されると思います。
goog.ui.datepickerを使おうと思ったらgoog.i18n.datetimesymbols.js(約150kB)がインクルードされて泣きました。
SIMPLE_OPTIMIZATIONSだと半分位のファイルサイズにしかなりませんし、その他文字コード絡みで色々と苦労させられたファイルでした。
確かにADVANCED_OPTIMIZATIONSが使えるとよいのですが、制約が強くて使えていません。
closure libraryでリサイズ用クラス
を公開します。
と言ってもgoog.fx.Draggerを張り付けてサイズ変えているだけなんですが。
jQuery UIのResizableクラスやExt.jsのExt.Resizableみたいなやつです。
textareaやimageには対応していないためdivでwrapしてください。
リサイズする要素にheightやwidthが設定されていないとIE6でうまく動かないケースがあったので注意してください。
- Category(s)
- JavaScript
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/uchida/closure-library30ea30b530a4752830af30e930b9/tbping
JavaScript的startsWith
goog.string.startsWith = function(str, prefix) {
return str.indexOf(prefix) == 0;
};
これだとstrが長い文字列でしかもprefixがstr内になかった場合に無駄な探索処理をやる羽目になります。
代わりに正規表現やsubstringを使って比較をすれば処理速度の悪化は避けられそうですが、正規表現や文字列のオブジェクト生成コストが気になります。
そこでlastIndexOfを使って書けば無駄な探索もオブジェクト生成も避けられるはずです。
str.lastIndexOf(prefix, 0) == 0;
もちろんendsWithもindexOfで書き換え可能です。
測ってみました。
検証コードstartswith.zip
A: 'abc'.startsWith('ab')を100000回
B: ('x' * 10000)+'abc').startsWith('ab')を10000回
IE6(A) | IE6(B) | FireFox3.5(A) | FireFox3.5(B) | |
---|---|---|---|---|
indexof | 125ms | 266ms | 3ms | 459ms |
lastindexof | 141ms | 16ms | 5ms | 1ms |
regexp | 297ms | 47ms | 29ms | 608ms |
substring | 234ms | 15ms | 15ms | 2ms |
文字列が短いときはindexOf版に及びませんが、予想通り長くなったときの性能悪化はなくなりました。
少し意外だったのはFireFoxの正規表現版がO(n)だったところでしょうか。
CなんかだとcharAtで一文字ずつ調べた方が速そうですが、文字列生成のコストが高そうな気がしたのでやりませんでした。
fujitaさんが測定してくれた結果ではこうなりました。Chorme速いなあ。
Safari(A) | Safari(B) | Chrome(A) | Chrome(B) | FF(A) | FF(B) | |
---|---|---|---|---|---|---|
indexof | 6ms | 97ms | 8ms | 198ms | 3ms | 412ms |
lastindexof | 6ms | 0ms | 9ms | 1ms | 4ms | 1ms |
regexp | 21ms | 67ms | 13ms | 1ms | 19ms | 547ms |
substring | 17ms | 2ms | 14ms | 1ms | 9ms | 0ms |
まあJavaScriptで10000文字もあるような文字列を扱うことは稀でしょうけど。
- Category(s)
- JavaScript
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/uchida/javascript7684startswith/tbping
Re:JavaScript的startsWith
Re:JavaScript的startsWith
https://groups.google.com/group/closure-library-discuss/browse_thread/thread/d0eed33a86410
Re:JavaScript的startsWith
つまり"hogehoge".starttsWith("hoge")がfalseになってしまうと思います。
Re:JavaScript的startsWith
> つまり"hogehoge".starttsWith("hoge")がfalseになってしまうと思います。
lastIndexOfの第2引数に0を指定してるため、'hogehoge'.lastIndexOf('hoge', 0)の結果が0になります。
JavaScript的startsWith 続き
文字列が短いときはindexOf版に及びませんが、私はこれを単に、lastIndexOf版はindexOf版より引数が増えたため範囲チェック等の処理が増えたんだろうと思っていました。
がendsWithを調べたところ、どうもそれだけではないようです。
goog.string.endsWith = function(str, suffix) {closureのendsWithはご覧のとおりlastIndexOfを読んでいます。
var l = str.length - suffix.length;
return l >= 0 && str.lastIndexOf(suffix, l) == l;
};
startsWithでは逆にこれをindexOfに書き換えれば無駄な探索がなくなる、しかも引数の数が同じなので見つけた場合の速度も変わらないはず。
で、これが結果です。
fujitaさんにJSLitmus.jsというツールを教えてもらったのでそれを使ってます。数字が大きいほど速い処理になります。
lastIndexOf | indexOf | |
---|---|---|
abcdefghi endsWith hi | 16205537 | 8256192 |
abcdefghi endsWith h* | 10876803 | 7332453 |
abcdefghijklmnopqrstyuwx endsWith wx | 13090052 | 7744009 |
abcdefghijklmnopqrstyuwx endsWith w* | 6586334 | 7931452 |
abcdefghijklmnopqrstyuwxyzABCDEFGHIJKLM endsWith LM | 14726198 | 6611250 |
abcdefghijklmnopqrstyuwxyzABCDEFGHIJKLM endsWith L* | 5185713 | 7813253 |
前回はあまりに極端なパターンだったのでstartsWithの方も測り直しています。
startswith-endswith-benchmark-20100123.zip
startswith-endswith-benchmark-results-20100123.zip
文字数が少ない場合にindexOf版endsWithが遅くなるのは予想外でした。
分岐予測の失敗でもしているのでしょうか?
アルゴリズムから見ればlastIndexOf版startsWithやindexOf版endsWithの方が速くなりそうに見えるんですが。
なんか納得いかないなあ。
- Category(s)
- JavaScript
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/uchida/javascript7684startswith-7d9a304d/tbping
closure libraryでのクラス定義
Ariel = function() {ライブラリを使用しているところが一つもありませんね。
this.name = 'ariel';
};
Ariel.prototype.toString = function() {
return this.name;
};
Ariel.prototype.popup = function() {
alert(this.toString());
};
継承するにはgoog.inheritsを使います。
ArielNT = function() {
ArielNT.superClass_.constructor.call(this);
};
goog.inherits(ArielNT, Ariel);
ArielNT.prototype.toString = function() {
return ArielNT.superClass_.toString.apply(this, arguments) + '-networks';
};
prototype.jsのClass.createや$superに比べると随分と冗長です。
ただし$superの場合FunctionをtoStringして解析したりしているので、速度的にはclosure libraryの方が有利なのかもしれません。
最新版のclosure libraryにはgoog.baseと言うメソッドが追加されています。
これを使うとさっきのArielNTクラスは以下のように書けます。
ArielNT = function() {
goog.base(this);
};
goog.inherits(ArielNT, Ariel);
ArielNT.prototype.toString = function() {
return goog.base(this, 'toString') + '-networks';
};
そしてArielNTクラスを定義したファイル(ariel-nt.js)を作成して、これをclosure compilerに渡してみましょう。
$ java -jar compiler.jar -js ariel-nt.js -js_output_file ariel-nt-compiled.js読みやすいようにインデントします。
ArielNT = function(){goog.baseを展開してくれています。
Ariel.call(this)
};
goog.inherits(ArielNT, Ariel);
ArielNT.prototype.toString = function(){
return ArielNT.superClass_.toString.call(this) + "-networks"
};
ちなみにgoog.baseを展開してくれるのは上記のようにArielNT.prototypeに一つずつメソッドやフィールドを追加した場合だけです。
例えばprototype.jsみたいに
goog.mixin(ArielNT.prototype, {と書くとコンパイル時にエラーになります。(goog.mixinはprototype.jsのObject.extend)
toString: function() {
return goog.base(this, 'toString') + '-networks';
}
});
ariel-nt.js:8: ERROR - incorrect use of goog.base: Could not find enclosing method.
return goog.base(this, 'toString') + '-networks';
^
1 error(s), 0 warning(s)
- Category(s)
- JavaScript
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/uchida/closure-library306e30af30e930b95b9a7fa9/tbping
closure libraryのgoog.dom.$は非推奨
普通に使ってました。
closure compiler で --compilation_level ADVANCED_OPTIMIZATIONS すればメソッド名の長さが問題にならないというのも一因でしょうか。
ただADVANCED_OPTIMIZATIONSは使っていないクラスやメソッドを消してしまうため、例えば巨大なJSPを使っていたりすると今のところ現実的でないと思っています。
ただし私は closure compiler のフラグの全てを理解しているわけではないので、もしかしたら何か方法があるのかもしれません。
- Category(s)
- JavaScript
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/uchida/closure-library306egoog-dom-306f975e63a85968/tbping
IE + closure library + cloneNode
http://code.google.com/p/closure-library/source/detail?r=43
これまでgoog/demos/css以下にあったcssがgoog/cssに昇格したようです。
デモ用のcssをいちいちコピーするのが面倒だったので助かります。
さて、IEだとcloneNode時にイベントや独自に追加したプロパティも一緒にコピーされてしまいます。
このせいでclosure libraryのイベントで厄介な問題につまづきました。
備忘録兼腹いせとしてここに記録しておきます。
<html>
<head>
<script type="text/javascript" src="../closure-library-read-only/closure/goog/base.js"></script>
<script>
goog.require('goog.events');
goog.require('goog.dom');
goog.require('goog.events.EventType');
goog.require('goog.events.EventHandler');
</script>
</head>
<body>
<div id="hoge">hoge</div>
<script>
var onMouseDown = function(e) {
var target = e.target || e.srcElement;
alert(target.id);
};
var onMouseDownEx = function(e) {
var target = e.target || e.srcElement;
alert(target.id + ' Ex');
};
goog.events.listen(goog.dom.getElement('hoge'), goog.events.EventType.MOUSEDOWN, onMouseDown, false);
// goog.dom.getElement('hoge').attachEvent('onmousedown', onMouseDown);
// "hoge" をコピーして "fuga" を作る
var clone = goog.dom.getElement('hoge').cloneNode(true);
clone.id = 'fuga';
clone.innerHTML = 'fuga';
document.body.appendChild(clone);
// IEだとイベントもコピーされているため外す
goog.events.unlisten(clone, goog.events.EventType.MOUSEDOWN, onMouseDown, false);
// clone.detachEvent('onmousedown', onMouseDown);
// 別のイベントをつける
goog.events.listen(goog.dom.getElement('fuga'), goog.events.EventType.MOUSEDOWN, onMouseDownEx, false);
// goog.dom.getElement('fuga').attachEvent('onmousedown', onMouseDownEx);
</script>
</body>
</html>
id="hoge"のdiv要素をcloneNodeして別のイベントをlistenしています。
これでIEでもFireFoxでも"hoge"を押下したら「hoge」が、"div"を押下したら「fuga Ex」が表示されるように見えます。
しかしIEでは"hoge"を押下しても何も表示されません。
理由はgoog.events.listen/unlistenの中で使っているgoog.getHashCodeにあります。
http://code.google.com/p/closure-library/source/browse/trunk/closure/goog/base.js
/**こいつは見ての通り与えられたオブジェクトにプロパティを追加し、そこにユニークなハッシュコード(という名の連番)をセットしてます。
* Adds a hash code field to an object. The hash code is unique for the
* given object.
* @param {Object} obj The object to get the hash code for.
* @return {number} The hash code for the object.
*/
goog.getHashCode = function(obj) {
// In IE, DOM nodes do not extend Object so they do not have this method.
// we need to check hasOwnProperty because the proto might have this set.
// TODO: There is a proposal to add hashcode as a global function to JS2
// we should keep track of this process so we can use that whenever
// it starts to show up in the real world.
if (obj.hasOwnProperty && obj.hasOwnProperty(goog.HASH_CODE_PROPERTY_)) {
return obj[goog.HASH_CODE_PROPERTY_];
}
if (!obj[goog.HASH_CODE_PROPERTY_]) {
obj[goog.HASH_CODE_PROPERTY_] = ++goog.hashCodeCounter_;
}
return obj[goog.HASH_CODE_PROPERTY_];
};
goog.events.listen/unlistenではこのハッシュコードでunlistenするイベントハンドラを取得したり、同じ全くイベントを監視しないようにしたりしています。
IEだとcloneNode時にこのハッシュコードもコピーしてしまうため、イベント管理まわりで思ったように動かないことがあります。
IEがタコなのか、closure libraryがまずいのか、cloneNodeが黒魔術なのか。
怖いなあ、これ。
- Category(s)
- JavaScript
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/uchida/ie-closure-library-clonenode/tbping
Re:Google Closureを使ってみるかも (1)
https://groups.google.com/group/closure-library-discuss/browse_thread/thread/7291e569602415a8