javascript
Up one levelJavaScript の for イディオム
var array = [...];
for (var i = 0, len = array.length; i < len; i++) {
var elem = array[i];
...
}
は有名ですが、もう少し JavaScript っぽい書き方があります。
var array = [...];
for (var i = 0, elem; elem = array[i]; i++) {
...
}
コードが一行へるし若干こちらのほうが高速です。配列の要素に数値などが入る場合は (elem = array[i]) != null という条件にしないとまずいです。
蛇足ですが、
var self = this;
var array = $R(0, 100).map(function(a) { return a + self.offset });
って
var array = $R(0, 100).map((function(a) { return a + this.offset }).bind(this));
って書けばいいんですね。気づきませんでした。
- Category(s)
- program
- javascript
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/javascript-for-idiom/tbping
Ajax 最適化 Tips - Prototype.js のパフォーマンス
prototype.js 1.6.0 RC で、カスタムイベントが使えるようになったり、ネイティブの forEach が使われるようになったり、簡単に DOM エレメントを構築できるようになったり、 isNumber で数値型かチェックできるようになったり、すばらしいバージョンアップであることは間違いないのですが、 prototype.js のソースを見てもわかるとおり、パフォーマンス云々よりも JavaScript 的にスマートな書き方・より単純で意味の取りやすい書き方が優先されており(もちろんこれは正しい方向)、あまり熟知せずにこれらの関数を乱用してしまうとアプリケーションのパフォーマンスに甚大な被害を与えてしまうことがあります。今回は prototype.js を使うにあたって(パフォーマンス的に)注意すべき点を紹介したいと思います。なお参考にする prototype.js のバージョンは 1.5.1 です。
Element.extend
Element.extend 自体それほど重い処理をやっているわけではないのですが、呼び出し関数が多いため自ずと慢性的なパフォーマンスの悪化をもたらします。しかし、 prototype.js ではエレメントであるようなオブジェクトに対しては即刻 $() を適用して間接的に Element.extend を実行しているので、 prototype.js を使う限りこの問題をさけることはできません(現在アリエルで開発しているプロダクトも Firebug でプロファイルを取ると必ず Element.extend が上位に食い込んでくるのです)。個人的には一番最適化してほしい関数です。ぱっと見るかぎりでは最適化の余地は十分ありそうですし。
Element.extend = function(element) {
var F = Prototype.BrowserFeatures;
if (!element || !element.tagName || element.nodeType == 3 ||
element._extended || F.SpecificElementExtensions || element == window)
return element;
var methods = {}, tagName = element.tagName, cache = Element.extend.cache,
T = Element.Methods.ByTag;
// extend methods for all tags (Safari doesn't need this)
if (!F.ElementExtensions) {
Object.extend(methods, Element.Methods),
Object.extend(methods, Element.Methods.Simulated);
}
// extend methods for specific tags
if (T[tagName]) Object.extend(methods, T[tagName]);
for (var property in methods) {
var value = methods[property];
if (typeof value == 'function' && !(property in element))
element[property] = cache.findOrStore(value);
}
element._extended = Prototype.emptyFunction;
return element;
};
Array.from
var $A = Array.from = function(iterable) {
if (!iterable) return [];
if (iterable.toArray) {
return iterable.toArray();
} else {
var results = [];
for (var i = 0, length = iterable.length; i < length; i++)
results.push(iterable[i]);
return results;
}
}
Array.from は Enumerable を配列にする関数ですが Array のメソッドを使いたいがためについつい以下のようなことをしてしまいます。
var a = $A(arguments).last();
これは本来
var a = Array.last.apply(arguments);
と書くべきです。一般的に破壊的な操作をしない限り Array.from を使うべきではありません。
var args = $A(arguments); var a = args.shift(); return a.apply(this, args);
Array.without
個人的に prototype.js から一番外してほしい関数が Array.without です(関数型言語的に見れば良い関数なのですが)。
without: function() {
var values = $A(arguments);
return this.select(function(value) {
return !values.include(value);
});
},
外してほしい理由は主に Array.without が安易に使われてしまうからです。なので本質的には Array.without が悪いわけではありません。
僕が知らないだけなのかもしれませんが、 JavaScript では配列から特定の要素を削除する単純な手段がありません。
一般的な手段は、
var a = [1, 2, 3]; a.splice(a.indexOf(2), 1);
になるのですが、こんな気持ち悪いコード書けない、ということで Array.without を使ってしまうのです。 prototype.js 1.6.1 RC でもそれらしい関数がないですし、もしかしたら何かちゃんとした方法がすでにあって、ただそれを自分が知らないだけじゃないのかという不安にかられてしまうほど、 Array.remove なる関数が存在しないことに疑問を覚えてしまいます。
Element.update
update: function(element, html) {
html = typeof html == 'undefined' ? '' : html.toString();
$(element).innerHTML = html.stripScripts();
setTimeout(function() {html.evalScripts()}, 10);
return element;
},
エレメントの中身を書きかえようとして Element.update を使うのは好ましくありません。ソースを見てもわかるように、 script タグの削除・その script の評価(しかも setTimeout で)をやるからです。 Element.update は script タグが含まれる可能性がある場合のみに使うようにしてください。それ以外は普通に innerHTML への代入で問題ありません。
Element.getElementsBySelector
規模が大きいのでソースは示しませんが、 document.evaluate が使えない環境( IE6 など)で、巨大なエレメントに Element.getElementsBySelector を実行すると大変なことになります。ほとんどの場合、 document.getElementById と document.getElementsByClassName の組み合わせで書き換え可能です。
Element.up
up: function(element, expression, index) {
element = $(element);
if (arguments.length == 1) return $(element.parentNode);
var ancestors = element.ancestors();
return expression ? Selector.findElement(ancestors, expression, index) :
ancestors[index || 0];
},
引数がない場合は問題ありませんが、引数がある場合は、全ての祖先ノードを取得して Selector.findElement (こいつがまた遅い) を適用します。ほとんどの場合、小規模関数の組み合わせで書き換え可能です。
Element.down
down: function(element, expression, index) {
element = $(element);
if (arguments.length == 1) return element.firstDescendant();
var descendants = element.descendants();
return expression ? Selector.findElement(descendants, expression, index) :
descendants[index || 0];
},
引数がない場合は問題ありませんが、引数がある場合は、全ての子孫ノードを取得して Selector.findElement (こいつがまた遅い) を適用します。ほとんどの場合、 document.getElementById と document.getElementsByClassName の組み合わせで書き換え可能です。 Element.up よりこちらのほうが断然遅くなる可能性があるので気を付けてください。
Element.previous & Element.next
previous: function(element, expression, index) {
element = $(element);
if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
var previousSiblings = element.previousSiblings();
return expression ? Selector.findElement(previousSiblings, expression, index) :
previousSiblings[index || 0];
},
next: function(element, expression, index) {
element = $(element);
if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
var nextSiblings = element.nextSiblings();
return expression ? Selector.findElement(nextSiblings, expression, index) :
nextSiblings[index || 0];
},
Element.up とほとんど同じ。
余談
以上です。生半可なエントリで申し訳ないです。言うまでもありませんが、無駄な最適化はバグを生むだけです。まずは存分に prototype.js の機能を使って、それからプロファイルを取って適切にボトルネックを取り除きましょう。ただ Element.getElementsBySelector なんかは最初から使うべきでないぐらいの代物ですが。
- Category(s)
- program
- javascript
- ajax
- performance tuning
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/ajax-tips-prototype-js/tbping
Ajax 最適化 Tips - getElementById する前に
getElementById で要素を手っ取り早く取得するのはとても良いイディオムですが(コードが短かくなる)、パフォーマンスのことを心配する場合は以下のことを念頭に入れておくと良いかもしれません。
Microsoft の DHTML Collections(*) によると、
(*) http://msdn2.microsoft.com/en-us/library/ms533048.aspx
document 以下の特定の種類の要素をフラットにアクセスできるコレクションという形で提供しています。通常ノードツリーをトラバースするよりコレクションにアクセスするほうが速いので、これを使わない手はありません。特に便利だと思われるのは以下のようなものでしょうか(他にも便利なものがありますが、今回は document に対してのコレクションのみに限定したいので省きました)。
- document.anchors
- document.forms
仮に以下のような HTML があるとします。
<a id="button1">Click me!</a> <form id="form1"> ... </form>
この場合、
document.getElementById('button1').innerHTML = 'Do not click me!';
document.getElementById('form1').onsubmit = function() { alert('Submit!') };
と書くより、
document.anchors['button1'].innerHTML = 'Do not click me!';
document.forms['form1'].onsubmit = function() { alert('Sumit!') };
と書くほうが格段に速いです。ただコレクションのインデックスは整数値あるいは id/name を受けつける寛大な仕様になっているので、もう少し厳密にするなら以下のように書くと良いかもしれません。
function getElementByIdWithHint(id, expectedTagName) {
expectedTagName = (expectedTagName || '').toLowerCase();
var collection;
if (expectedTagName == 'a')
collection = document.anchors;
else if (expectedTagName == 'form')
collection = document.forms;
if (collection) {
for (var i = 0, len = collection.length; i < len; i++) {
var elem = collection[i];
if (elem.id == id)
return elem;
}
return null;
} else
return document.getElementById(id);
}
getElementByIdWithHint('button1', 'a').innerHTML = 'Do not click me!';
getElementByIdWithHint('form1', 'form').onsubmit = function() { alert('Sumit!') };
言うまでもありませんが、 getElementById を使わないにこしたことはありません。ほとんどの場合はうまくキャッシュさせたり、もっとオーダーの低い firstChild なりを使って何とかなったりします。その辺りのこともいずれ話したいと思います。
- Category(s)
- javascript
- performance tuning
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/before-getElementById/tbping