Personal tools
You are here: Home ブログ uchida Categories JavaScript
« December 2010 »
Su Mo Tu We Th Fr Sa
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  
Categories
JavaScript
Go
Ada
Delphi
junk
 
Document Actions

JavaScript

Up one level

Document Actions

Google 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

Re:Google Closureを使ってみるかも (1)

Posted by uchida at 2009-11-11 19:06
FireFox2がサポート外の予定らしい
https://groups.google.com/group/closure-library-discuss/browse_thread/thread/7291e569602415a8

closure libraryのBrowserEventが微妙

以前closure libraryのdialogがリサイズできないと書きました。
しかたがないのでgoog.fx.Draggerを使ってExt.jsjQuery 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;
this.screenY = e.screenY || 0;
の2行が全体の80%くらい時間がかかっている事が判明しました。
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が微妙

Posted by amachang at 2010-05-11 18:02
検索から辿りつきました。

私も、最近 Closure Library を使っていて、同じようなことを思ったんですが、
きっと Closure Library って Closure Compiler とセットで使うことを主眼に置いてるんじゃないでしょうか。

Closure Compiler を使って強い最適化をすれば、おそらく「使わないプロパティへの値の設定」は省略されると思います。

まあ Closure Compiler を使わないとパフォーマンスが出ないっていうのも微妙だとは思いますが。

Re:closure libraryのBrowserEventが微妙

Posted by uchida at 2010-05-17 19:12
amachangさん
いつもブログの方拝見させてもらっています。

> 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でうまく動かないケースがあったので注意してください。

goog-ui-resizable.zip

Category(s)
JavaScript
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/uchida/closure-library30ea30b530a4752830af30e930b9/tbping

JavaScript的startsWith

closureのgoog.string.startsWithやprototype.jsのString.startsWithではindexOfを呼ぶコードになってます。
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)
indexof125ms266ms3ms459ms
lastindexof141ms16ms5ms1ms
regexp297ms47ms29ms608ms
substring234ms15ms15ms2ms

文字列が短いときはindexOf版に及びませんが、予想通り長くなったときの性能悪化はなくなりました。
少し意外だったのはFireFoxの正規表現版がO(n)だったところでしょうか。
CなんかだとcharAtで一文字ずつ調べた方が速そうですが、文字列生成のコストが高そうな気がしたのでやりませんでした。

fujitaさんが測定してくれた結果ではこうなりました。Chorme速いなあ。


Safari(A)Safari(B)Chrome(A)Chrome(B)FF(A)FF(B)
indexof6ms97ms8ms198ms3ms412ms
lastindexof6ms0ms9ms1ms4ms1ms
regexp21ms67ms13ms1ms19ms547ms
substring17ms2ms14ms1ms9ms0ms

まあJavaScriptで10000文字もあるような文字列を扱うことは稀でしょうけど。

Category(s)
JavaScript
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/uchida/javascript7684startswith/tbping

Re:JavaScript的startsWith

Posted by inoue at 2010-01-22 01:46
すばらしい。Google越えました。

Re:JavaScript的startsWith

Posted by uchida at 2010-01-25 23:30
googleを越えるのはなかなか難しそうです。
https://groups.google.com/group/closure-library-discuss/browse_thread/thread/d0eed33a86410

Re:JavaScript的startsWith

Posted by 2323 at 2010-03-24 16:41
素朴な疑問ですがlastIndexOf()だとstr中にprefixが2回以上ある場合に正しくならないのでは?
つまり"hogehoge".starttsWith("hoge")がfalseになってしまうと思います。

Re:JavaScript的startsWith

Posted by uchida at 2010-04-26 14:51
> 素朴な疑問ですがlastIndexOf()だとstr中にprefixが2回以上ある場合に正しくならないのでは?
> つまり"hogehoge".starttsWith("hoge")がfalseになってしまうと思います。

lastIndexOfの第2引数に0を指定してるため、'hogehoge'.lastIndexOf('hoge', 0)の結果が0になります。

JavaScript的startsWith 続き

http://dev.ariel-networks.com/Members/uchida/javascript7684startswith/
文字列が短いときはindexOf版に及びませんが、
私はこれを単に、lastIndexOf版はindexOf版より引数が増えたため範囲チェック等の処理が増えたんだろうと思っていました。
がendsWithを調べたところ、どうもそれだけではないようです。

goog.string.endsWith = function(str, suffix) {
var l = str.length - suffix.length;
return l >= 0 && str.lastIndexOf(suffix, l) == l;
};
closureのendsWithはご覧のとおりlastIndexOfを読んでいます。

startsWithでは逆にこれをindexOfに書き換えれば無駄な探索がなくなる、しかも引数の数が同じなので見つけた場合の速度も変わらないはず。

で、これが結果です。
fujitaさんにJSLitmus.jsというツールを教えてもらったのでそれを使ってます。数字が大きいほど速い処理になります。


lastIndexOfindexOf
abcdefghi endsWith hi162055378256192
abcdefghi endsWith h*108768037332453
abcdefghijklmnopqrstyuwx endsWith wx130900527744009
abcdefghijklmnopqrstyuwx endsWith w*65863347931452
abcdefghijklmnopqrstyuwxyzABCDEFGHIJKLM endsWith LM147261986611250
abcdefghijklmnopqrstyuwxyzABCDEFGHIJKLM endsWith L*51857137813253

前回はあまりに極端なパターンだったので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(){
Ariel.call(this)
};
goog.inherits(ArielNT, Ariel);

ArielNT.prototype.toString = function(){
return ArielNT.superClass_.toString.call(this) + "-networks"
};
goog.baseを展開してくれています。

ちなみにgoog.baseを展開してくれるのは上記のようにArielNT.prototypeに一つずつメソッドやフィールドを追加した場合だけです。
例えばprototype.jsみたいに

goog.mixin(ArielNT.prototype, {
toString: function() {
return goog.base(this, 'toString') + '-networks';
}
});
と書くとコンパイル時にエラーになります。(goog.mixinはprototype.jsのObject.extend)
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.$は非推奨

http://code.google.com/p/closure-library/issues/detail?id=87&can=1

普通に使ってました。
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

Copyright(C) 2001 - 2006 Ariel Networks, Inc. All rights reserved.