Upload
soichi-takamura
View
3.109
Download
6
Embed Size (px)
Citation preview
goog.ui.Componentのはぐれ方 アリエル・ネットワーク開発部 高村
私 高村壮一(@stakamur)
HTMLとCSSをひたすら書く↓
jQuery↓
vimとのであい↓
Closure Library↓
こないだから Sencha Touch ..
開発部UIチームで働いています
github projectspiglovesyou / flickGal (jQuery)piglovesyou / closure-scrollerpiglovesyou / closure-thousandrows
yan-yan-yahuoku.com
・・・あとは、Closure Library を使って作ったウェブサービスを、最近公開しました。ヤンヤンヤフオクといって、ヤフオクの大量の商品を、軽い動作で一覧できるのが特徴です。
このサービスを作る前に、ひとつ試したいことがありました。それが、
goog.ui.Component インスタンスを徹底的にツリー化しよう
・・ということです。
/** @constructor */app.ui.Component = function () { goog.base(this);
var child = new app.ui.Another(); this.addChild(child);};...
ツリー構造とは、画面の各構成部分を全てui.Componentで管理させた上での、
それらの親子関係のことです。
app
child child
childchild
ツリー化することは、とくべつなアイデアではない。
むしろ、Closure LibraryにはaddChildなどのメソッドがあることから、奨励していると思う
http://tiny-word.appspot.com
伊藤 千光さん が書かれた、Closure 本のデモでも、
コンポーネントのツリー化を基礎に設計されています。
ツリー化は、基本。(例外はやまほどあるだろう)
しかし、ツリー構造を作りにくいときも・・
ルールがあるところに例外はつきもの。
ルールから外れたときこそ、フレームワークの真価が問われる。
そこで、今日お話したいのは、・・・
前半:なぜ、goog.ui.Componentをツリー化すべきなのか?
後半:goog.ui.Componentのはぐれかたツリー化する上での例外ケース3種類
なぜ、goog.ui.Componentをツリー化すべきなのか?
後半:goog.ui.Componentのはぐれかたツリー化する上での例外ケース3種類
前半:なぜ、goog.ui.Componentをツリー化すべきなのか?
はじめに:goog.ui.Componentの簡単な説明
ui.Component は、UIモジュールです。
http://closure-library.googlecode.com/svn/docs/class_goog_ui_Component.html
クラス関係です。
http://closure-library.googlecode.com/svn/trunk/closure/goog/demos/index.html
インターフェース
var component = new app.ui.Component();component.render();
インターフェースです。
どこかのコードで、こう書くと、componentは自分でelementを作り出し、同時にdocument.body 配下にelementをappendします。
var component = new app.ui.Component();
var el = goog.dom.getElement(‘component-wrapper’);component.decorate(el);
似た機能で、decorate があります。elementのcreateとappendのコストをはぶけ、パフォーマンスの向上が見込めます。
しかし、アプリではelementの動的な生成が基本なので、decorateに関しては今日は触れません。
実装
app.ui.Component.prototype.createDom = function () {
var dh = this.getDomHelper();
this.setElementInternal(dh.createDom('div', null));
};
app.ui.Component.prototype.enterDocument = function () {
goog.base(this, ‘enterDocument’);
this.getHandler().listen(this.getElement(),
‘click’, function() {(‘handle on click’);});
};
createDom では、自分が管理するelementを作り、それをメンバーにセットします。
enterDocumentは、elementが必ずあることが前提のコードを書く場所です。
なぜ、element生成とenterDocumentが分かれているのか
コンストラクタ
↓createDom
↓enterDocument
↓exitDocument
↓dispose
elementがないと、エラーになってしまう処理を、enterDocumentに集めることで、ui.Componentの安全で効率的なライフサイクルを提供できるからです。
コンストラクタ
↓createDom
↓enterDocument
↓exitDocument
↓dispose
• DOM Exception を防ぐ• enter したときだけ
listener を持たせられる(exit したら外せる)
このステップを踏むことで、ブラウザJavascriptにありがちなDOM Exception を防ぐことができます。
あとは、exitDocumentで、よけいなリスナ関数を除去することで、パフォーマンス、メモリ効率も上げられます。
goog.ui.Componentの簡単な説明 おわり
前半:なぜ、goog.ui.Componentをツリー化すべきなのか?
あらためまして。
3つの理由
逆に、もし大量のインスタンスをツリーで管理しなければ、どうなる?
http://closure-library.googlecode.com/svn/docs/class_goog_ui_Component.html
まず、スーパークラスであるgoog.Disposable の恩恵を受けられなくなります。
var component = new app.ui.Component();
(‘...’);
component.dispose();
goog.Disposeは、もっともベーシックな破棄機能を提供します。
var component = new app.ui.Component();
(‘...’);
component.dispose(); // 内部オブジェクトの参照の破棄
// element と参照の破棄
// リスナの破棄
// 子インスタンスのdispose
disposeの実装のおかげで、インスタンスは自分に責任のがる他のオブジェクトを確実に破棄することができます。
更に、ツリー構造にしておけば、子インスタンスのdisposeも走らせてくれます。
理由その1:(ツリーにしないと)関連するインスタンスのdisposeが、
大変になる。
もし
http://closure-library.googlecode.com/svn/docs/class_goog_ui_Component.html
次に、EventTarget の恩恵も得られなくなります。
var component = new goog.ui.Component();
goog.events.listen(component, 'shout', function(e) {
console.log('shut up!!!');
});
component.dispatchEvent('shout');
EventTargetは、自身をelementのようなイベントターゲットのターゲットにします。
これにより、Observer パターンを提供します。
大量のインスタンスが、お互いに通信し合う必要があったら?
では、もし大量のインスタンス同士が・・・?
大変です。
参照をみつけだし、ひとつひとつlistenするのは大変・・・
listen listen listen
まず、イベントをlistenするために参照を得なければなりません。また、インスタンスの数だけリスナが必要になるでしょう。
シングルトンのEventTarget を利用したりもするかも知れない。それはとてもいいことだけど、いつもそれをするとイベントが複雑になりすぎる。
var parent = new goog.ui.Component();
goog.events.listen(parent, 'shout', function(e) { ('shut up!') });
for (var i=0; i<10; i++) {
var child = new goog.ui.Component();
child.setParentEventTarget(parent);
}
child.dispatchEvent('shout');
それを解決するのが、EventTarget のメソッドである、setParentEventTargetです。
これにより、インスタンス同士の関係を築くことができます。
parent
child.dispatchEvent()
capture
bubbling
DOMと同じ。子で発生したイベントは、ルートインスタンスまで通知されます。
listen は1回。イベントが勝手に飛んでくる。
listen
dispatch dispatch dispatch
listenは1回でok。子がdispatchしたイベントは、親のもとに自動的に集められます。
子は、親に処理をデリゲートできるので、できることも増えます。
listen
DOMイベントと同じ。Bubbling/Capture が利用可能
離れていても同様。DOMイベントと同じく、ルートにあたるインスタンスまでイベントは届きます。
理由その2:関連するインスタンス同士の通信が、効率的になる。
3つめ
毎回、レンダーツリーに変更を加えていませんか?
3つめの、理由です。
http://jsperf.com/appending-to-render-tree
http://jsperf.com/appending-to-render-tree
for (var i=0; i<10; i++) {
var component = new goog.ui.Component();
component.render(); // XXX: Don’t do this!
}
もしこう書いたら、jsperfの例と同じこと。
毎回レンダーツリーにelementをappendしていってしまっています。
関連するelement(DOMツリー)のappendは、1回で。
・・・こう実装すると、確実です。
/** @constructor */app.ui.Parent = function () { goog.base(this);
for (var i=0; i<10; i++) { this.addChild(new app.ui.Child()); }};
(‘...’);
まず、親のコンストラクタで、子のインスタンスを生成。
app.ui.Parent.prototype.createDom = function () {
(‘...’);
this.forEachChild(function(child) {
child.createDom();
dh.appendChild(
this.getContentElement(),
child.getElement());
}, this);
};
次に、親のcreateDom内で、子も一緒にcreateDomし、そのあとにappend先を決定します。
コンストラクタ
↓createDom
↓enterDocument
↓exitDocument
↓dispose
親コンポーネント 子コンポーネント
createDom、親のelementにappend
enterDocument
enterDocument
dispose
コンストラクタ手動
手動
自動
自動
自動
new と、createDom のみchildの挙動を指定をすれば、あとはgoog.ui.Componentが親コンポーネントのライフサイクルにそって、子も同じ運命をたどります。
コンストラクタ
↓createDom
↓enterDocument
↓exitDocument
↓dispose
親コンポーネント 子コンポーネント
createDom、親のelementにappend
enterDocument
enterDocument
dispose
コンストラクタ
parent.render()(bodyに、1度だけ
appendされる)
parent.dispose()
インターフェースから見る処理の流れは、こんな感じ。
したがって、子は基本的にrenderメソッドを使わない、といえると思います。(もちろん、使うこともできる)
理由その3:親のcreateDom → 子のcreateDom で、安全にDOMを組み立てられ、
効率よくappendすることができます。
その3まとめ。
parent.addChild(child, true); を使えばいいという方もいるかも知れませんが、基本形はこの形で考えます
( )
理由その1:インスタンスのdisposeが、大変になる。
理由その2:インスタンス同士の通信が、効率的になる。
理由その3:1回のappendで済む。 DOM Exception のリスクも減らせる。
以上3点が、「なぜツリー化するのか」の理由です。
ツリー化するメリットが理解いただけたかと思います
前半:なぜ、goog.ui.Componentをツリー化すべきなのか?
後半:goog.ui.Componentのはぐれかたツリー例外パターンX種類
十分理解していただいたところで。
なぜ、goog.ui.Componentをツリー化すべきなのか?
後半:goog.ui.Componentのはぐれかたツリー化する上での例外ケース3種類
後半は、この基本形からそれなければいけないケースを、3例、紹介したいと思います。
ツリーの基本形からはぐれる、3つのケースを紹介します
goog.ui.Thousandrows をちょっとだけ紹介させてください
まず、1つめを紹介する前に。
僕の作った、Thousandrows について紹介させてください。
• 大量の行を、すぐ表示• つなぎ目なしでスクロール(≠ページング)• 任意の箇所にジャンプ(≠単なる無限スクロール)
大量の行を効率よく表示できる、UIコンポーネントです。
http://stakam.net/closure/120722/
Thousandrows
Page
Row
コンポーネントの親子関係を築いているので、rowsのdisposeも効率的にでき、インスタンス間通信もしやすく、Thousandrowsが一体となって機能することができます。
goog.ui.Component↓
goog.ui.Control
↓goog.ui.Scroller
↓goog.ui.Thousandrows
このThousandrows を作るために、まずScrollerを作りました。継承させています。
Scroller
Slider
Scrollerは、こういう構造になっています。
ですが、Thousandrowsのようなものを作れるように、Sliderはchildrenに加えるわけにはいきません。
ケース1:関連するインスタンスを、childに加えられないことがある
goog.ui.Componentの children は、一律で contentElement_ にappendされるべき
Scroller というからには、childをcontentElement に入れていけるべきです。
Scroller
Slider
contentElement_
Scroller
Slider
Scrollerのchildにできない
contentElement_
child は、ユーザーが加えていけるようにするため、空にしておく必要がありました。
• childたちは、contentElement_ に並ぶようにしておかないと、 addChildAt が動かなくなる。(child.getElement()のsiblingに挿入してる)
•異質のcomponentインスタンスをchildrenに混ぜると、this.forEachChild しにくくなるのでおすすめできない。
Slider を、Scroller のchild にできない理由。
Scroller に関連する Slider コンポーネントを、child にできない。
childにできないことがわかった。こんなときは、どうするか?
childにせず、手動で親のライフサイクルに追従させればOK
childにせず、手動で親のライフサイクルに追従させるという手があります。
/** @constructor */goog.ui.Scroller = function () { goog.base(this); (‘...’);
this.slider_ = new goog.ui.Slider();};
(‘...’);
通常と同じ
goog.ui.Scroller.prototype.createDom = function () { (‘...’);
this.slider_.createDom(); this.getElement().appendChild(this.slider_.getElement());};
通常と同じ
goog.ui.Scroller.prototype.enterDocument = function () { (‘...’);
this.slider_.enterDocument(); // 忘れずに
this.getHandler().listen(this.slider_, ‘change’, function(e) { });
//(または、this.slider_.setParentEventTarget(this) してもいいと思う)
};
通常と違う
child でないので、enterDocument は手動で必要になるので、忘れずに。
あとは、イベントのlistenは、slider をターゲットにして行う必要があります。setParentEventTarget することも考えられますが、こうするとイベントがパブリックになります。
goog.ui.Scroller.prototype.exitDocument = function () { (‘...’);
this.slider_.exitDocument(); // なくてもいい};
通常と違う
goog.ui.Scroller.prototype.disposeInternal = function () { if (this.slider_) { this.slider_.dispose(); this.slider_ = null; }
(‘...’);};
通常と違う
ケース1まとめ:childにできない場合、手動で状態を変えていくことで
インスタンスを管理しつづけられる。
ケース2:childを、parnetの contentElement_ 以外に
appendしたいとき
再び、ちょっと紹介させてください
タブ
tab tab tab
frame
tabs
タブのchild は、frameひとつです。でも・・・
frame
childにしたい。。でもDOM的にとても距離がある
tab tab tabtabs
距離がある。tab のDOMにappend することは、考えられない。
append先だけ、変えればOK.
/** @constructor */app.ui.Tab = function () { goog.base(this);
this.frame_ = new app.ui.Frame();};
(‘...’);
通常と同じ
まず、親のコンストラクタで、子のインスタンスを生成しておきます。
app.ui.Tab.prototype.createDom = function () {
(‘...’);
this.frame_.createDom();
// 別のDOMに入れても問題ない。
dh.appendChild(
App.getInstance().getFrameWrapEl(),
this.frame_.getElement());
};
通常と違う
次に、親のcreateDom内で、子も一緒に
• childを、遠くのDOMにappendしても、特に問題ない• parentEventTarget として機能、disposeなども• getContentElement は、通ったらだめ。(addChildAt も)
注意点。
ケース2まとめ:DOM的関係性があまりなくても、
親子関係は築ける。
ケース3:ライフサイクルを意図的にずらしたいとき
先ほどの tab と frame の例は忘れて頂いて・・・
ここ。ちょっと雑です。具体例の使いかたが雑なので注意してください。
タブ
もういちど出て来ました。さっきの例は忘れてください。
非選択タブ。frameは、まだレンダリングしてない
タブ自身は、初期表示時にすべてレンダリングされますが、非選択のタブのフレームは、まだレンダリングしたくない。
親が選択されたときに、子をレンダリングしたい。
(意図的にライフサイクルをずらしたい)
/** @constructor */app.ui.Tab = function () { goog.base(this);
this.frame_ = new app.ui.Frame();};
(‘...’);
通常と同じ
まず、親のコンストラクタで、子のインスタンスを生成しておきます。
app.ui.Tab.prototype.createDom = function () {
(‘...’);
// frame はまだレンダリングしない。appendもされない。
};
通常と違う
createDom で、Tabのelement は作る。frame のelement は作らない。まだいらないから。
app.controller.Tab.prototype.processSelected = function () {
(‘...’);
if (!this.frame_.isInDocument()) {
this.frame_.render(wrapEl);
}
};
通常と違う
・・・タブである自分が選択されたタイミングで、初めてframeをレンダリングします。
append先は、前述したとおり、自由な場所を指定できます。
childにしてあるので、disposeなどは、親と同じときに自動で処理されます。
ケース3まとめ:必要に応じて、ライフサイクルを
ずらすことも可能。
• ケース1まとめ:childにできない場合、手動で状態を変えていくことでインスタンスを管理しつづけられる。
• ケース2まとめ:DOM的関係性があまりなくても、親子関係は築ける。
• ケース3まとめ:必要に応じて、ライフサイクルをずらすことも可能。
ツリー化のメリットと基本のライフサイクルを理解したうえで、柔軟にClosure Libraryからはぐれましょう!
おわり