100
goog.ui.Componentのはぐれ方 アリエル・ネットワーク開発部 高村

goog.ui.Component のはぐれかた

Embed Size (px)

Citation preview

Page 1: goog.ui.Component のはぐれかた

goog.ui.Componentのはぐれ方 アリエル・ネットワーク開発部 高村

Page 2: goog.ui.Component のはぐれかた

私 高村壮一(@stakamur)

HTMLとCSSをひたすら書く↓

jQuery↓

vimとのであい↓

Closure Library↓

こないだから Sencha Touch ..

開発部UIチームで働いています

Page 4: goog.ui.Component のはぐれかた

yan-yan-yahuoku.com

・・・あとは、Closure Library を使って作ったウェブサービスを、最近公開しました。ヤンヤンヤフオクといって、ヤフオクの大量の商品を、軽い動作で一覧できるのが特徴です。

このサービスを作る前に、ひとつ試したいことがありました。それが、

Page 5: goog.ui.Component のはぐれかた

goog.ui.Component インスタンスを徹底的にツリー化しよう

・・ということです。

Page 6: goog.ui.Component のはぐれかた

/** @constructor */app.ui.Component = function () { goog.base(this);

var child = new app.ui.Another(); this.addChild(child);};...

ツリー構造とは、画面の各構成部分を全てui.Componentで管理させた上での、

それらの親子関係のことです。

Page 7: goog.ui.Component のはぐれかた

app

child child

childchild

Page 8: goog.ui.Component のはぐれかた

ツリー化することは、とくべつなアイデアではない。

むしろ、Closure LibraryにはaddChildなどのメソッドがあることから、奨励していると思う

Page 9: goog.ui.Component のはぐれかた

http://tiny-word.appspot.com

伊藤 千光さん が書かれた、Closure 本のデモでも、

コンポーネントのツリー化を基礎に設計されています。

Page 10: goog.ui.Component のはぐれかた

ツリー化は、基本。(例外はやまほどあるだろう)

Page 11: goog.ui.Component のはぐれかた

しかし、ツリー構造を作りにくいときも・・

ルールがあるところに例外はつきもの。

ルールから外れたときこそ、フレームワークの真価が問われる。

そこで、今日お話したいのは、・・・

Page 12: goog.ui.Component のはぐれかた

前半:なぜ、goog.ui.Componentをツリー化すべきなのか?

後半:goog.ui.Componentのはぐれかたツリー化する上での例外ケース3種類

Page 13: goog.ui.Component のはぐれかた

なぜ、goog.ui.Componentをツリー化すべきなのか?

後半:goog.ui.Componentのはぐれかたツリー化する上での例外ケース3種類

Page 14: goog.ui.Component のはぐれかた

前半:なぜ、goog.ui.Componentをツリー化すべきなのか?

Page 15: goog.ui.Component のはぐれかた

はじめに:goog.ui.Componentの簡単な説明

ui.Component は、UIモジュールです。

Page 16: goog.ui.Component のはぐれかた

http://closure-library.googlecode.com/svn/docs/class_goog_ui_Component.html

クラス関係です。

Page 18: goog.ui.Component のはぐれかた

インターフェース

Page 19: goog.ui.Component のはぐれかた

var component = new app.ui.Component();component.render();

インターフェースです。

どこかのコードで、こう書くと、componentは自分でelementを作り出し、同時にdocument.body 配下にelementをappendします。

Page 20: goog.ui.Component のはぐれかた

var component = new app.ui.Component();

var el = goog.dom.getElement(‘component-wrapper’);component.decorate(el);

似た機能で、decorate があります。elementのcreateとappendのコストをはぶけ、パフォーマンスの向上が見込めます。

しかし、アプリではelementの動的な生成が基本なので、decorateに関しては今日は触れません。

Page 21: goog.ui.Component のはぐれかた

実装

Page 22: goog.ui.Component のはぐれかた

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が必ずあることが前提のコードを書く場所です。

Page 23: goog.ui.Component のはぐれかた

なぜ、element生成とenterDocumentが分かれているのか

Page 24: goog.ui.Component のはぐれかた

コンストラクタ

↓createDom

↓enterDocument

↓exitDocument

↓dispose

elementがないと、エラーになってしまう処理を、enterDocumentに集めることで、ui.Componentの安全で効率的なライフサイクルを提供できるからです。

Page 25: goog.ui.Component のはぐれかた

コンストラクタ

↓createDom

↓enterDocument

↓exitDocument

↓dispose

• DOM Exception を防ぐ• enter したときだけ

listener を持たせられる(exit したら外せる)

このステップを踏むことで、ブラウザJavascriptにありがちなDOM Exception を防ぐことができます。

あとは、exitDocumentで、よけいなリスナ関数を除去することで、パフォーマンス、メモリ効率も上げられます。

Page 26: goog.ui.Component のはぐれかた

goog.ui.Componentの簡単な説明 おわり

Page 27: goog.ui.Component のはぐれかた

前半:なぜ、goog.ui.Componentをツリー化すべきなのか?

あらためまして。

Page 28: goog.ui.Component のはぐれかた

3つの理由

Page 29: goog.ui.Component のはぐれかた

逆に、もし大量のインスタンスをツリーで管理しなければ、どうなる?

Page 30: goog.ui.Component のはぐれかた

http://closure-library.googlecode.com/svn/docs/class_goog_ui_Component.html

まず、スーパークラスであるgoog.Disposable の恩恵を受けられなくなります。

Page 31: goog.ui.Component のはぐれかた

var component = new app.ui.Component();

(‘...’);

component.dispose();

goog.Disposeは、もっともベーシックな破棄機能を提供します。

Page 32: goog.ui.Component のはぐれかた

var component = new app.ui.Component();

(‘...’);

component.dispose(); // 内部オブジェクトの参照の破棄

// element と参照の破棄

// リスナの破棄

// 子インスタンスのdispose

disposeの実装のおかげで、インスタンスは自分に責任のがる他のオブジェクトを確実に破棄することができます。

更に、ツリー構造にしておけば、子インスタンスのdisposeも走らせてくれます。

Page 33: goog.ui.Component のはぐれかた

理由その1:(ツリーにしないと)関連するインスタンスのdisposeが、

大変になる。

もし

Page 34: goog.ui.Component のはぐれかた

http://closure-library.googlecode.com/svn/docs/class_goog_ui_Component.html

次に、EventTarget の恩恵も得られなくなります。

Page 35: goog.ui.Component のはぐれかた

var component = new goog.ui.Component();

goog.events.listen(component, 'shout', function(e) {

console.log('shut up!!!');

});

component.dispatchEvent('shout');

EventTargetは、自身をelementのようなイベントターゲットのターゲットにします。

これにより、Observer パターンを提供します。

Page 36: goog.ui.Component のはぐれかた

大量のインスタンスが、お互いに通信し合う必要があったら?

では、もし大量のインスタンス同士が・・・?

大変です。

Page 37: goog.ui.Component のはぐれかた

参照をみつけだし、ひとつひとつlistenするのは大変・・・

listen listen listen

まず、イベントをlistenするために参照を得なければなりません。また、インスタンスの数だけリスナが必要になるでしょう。

シングルトンのEventTarget を利用したりもするかも知れない。それはとてもいいことだけど、いつもそれをするとイベントが複雑になりすぎる。

Page 38: goog.ui.Component のはぐれかた

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です。

これにより、インスタンス同士の関係を築くことができます。

Page 39: goog.ui.Component のはぐれかた

parent

child.dispatchEvent()

capture

bubbling

DOMと同じ。子で発生したイベントは、ルートインスタンスまで通知されます。

Page 40: goog.ui.Component のはぐれかた

listen は1回。イベントが勝手に飛んでくる。

listen

dispatch dispatch dispatch

listenは1回でok。子がdispatchしたイベントは、親のもとに自動的に集められます。

子は、親に処理をデリゲートできるので、できることも増えます。

Page 41: goog.ui.Component のはぐれかた

listen

DOMイベントと同じ。Bubbling/Capture が利用可能

離れていても同様。DOMイベントと同じく、ルートにあたるインスタンスまでイベントは届きます。

Page 42: goog.ui.Component のはぐれかた

理由その2:関連するインスタンス同士の通信が、効率的になる。

Page 43: goog.ui.Component のはぐれかた

3つめ

Page 44: goog.ui.Component のはぐれかた

毎回、レンダーツリーに変更を加えていませんか?

3つめの、理由です。

Page 47: goog.ui.Component のはぐれかた

for (var i=0; i<10; i++) {

var component = new goog.ui.Component();

component.render(); // XXX: Don’t do this!

}

もしこう書いたら、jsperfの例と同じこと。

毎回レンダーツリーにelementをappendしていってしまっています。

Page 48: goog.ui.Component のはぐれかた

関連するelement(DOMツリー)のappendは、1回で。

・・・こう実装すると、確実です。

Page 49: goog.ui.Component のはぐれかた

/** @constructor */app.ui.Parent = function () { goog.base(this);

for (var i=0; i<10; i++) { this.addChild(new app.ui.Child()); }};

(‘...’);

まず、親のコンストラクタで、子のインスタンスを生成。

Page 50: goog.ui.Component のはぐれかた

app.ui.Parent.prototype.createDom = function () {

(‘...’);

this.forEachChild(function(child) {

child.createDom();

dh.appendChild(

this.getContentElement(),

child.getElement());

}, this);

};

次に、親のcreateDom内で、子も一緒にcreateDomし、そのあとにappend先を決定します。

Page 51: goog.ui.Component のはぐれかた

コンストラクタ

↓createDom

↓enterDocument

↓exitDocument

↓dispose

親コンポーネント 子コンポーネント

createDom、親のelementにappend

enterDocument

enterDocument

dispose

コンストラクタ手動

手動

自動

自動

自動

new と、createDom のみchildの挙動を指定をすれば、あとはgoog.ui.Componentが親コンポーネントのライフサイクルにそって、子も同じ運命をたどります。

Page 52: goog.ui.Component のはぐれかた

コンストラクタ

↓createDom

↓enterDocument

↓exitDocument

↓dispose

親コンポーネント 子コンポーネント

createDom、親のelementにappend

enterDocument

enterDocument

dispose

コンストラクタ

parent.render()(bodyに、1度だけ

appendされる)

parent.dispose()

インターフェースから見る処理の流れは、こんな感じ。

したがって、子は基本的にrenderメソッドを使わない、といえると思います。(もちろん、使うこともできる)

Page 53: goog.ui.Component のはぐれかた

理由その3:親のcreateDom → 子のcreateDom で、安全にDOMを組み立てられ、

効率よくappendすることができます。

その3まとめ。

Page 54: goog.ui.Component のはぐれかた

parent.addChild(child, true); を使えばいいという方もいるかも知れませんが、基本形はこの形で考えます

(       )

Page 55: goog.ui.Component のはぐれかた

理由その1:インスタンスのdisposeが、大変になる。

理由その2:インスタンス同士の通信が、効率的になる。

理由その3:1回のappendで済む。 DOM Exception のリスクも減らせる。

以上3点が、「なぜツリー化するのか」の理由です。

Page 56: goog.ui.Component のはぐれかた

ツリー化するメリットが理解いただけたかと思います

Page 57: goog.ui.Component のはぐれかた

前半:なぜ、goog.ui.Componentをツリー化すべきなのか?

後半:goog.ui.Componentのはぐれかたツリー例外パターンX種類

十分理解していただいたところで。

Page 58: goog.ui.Component のはぐれかた

なぜ、goog.ui.Componentをツリー化すべきなのか?

後半:goog.ui.Componentのはぐれかたツリー化する上での例外ケース3種類

後半は、この基本形からそれなければいけないケースを、3例、紹介したいと思います。

Page 59: goog.ui.Component のはぐれかた

ツリーの基本形からはぐれる、3つのケースを紹介します

Page 60: goog.ui.Component のはぐれかた

goog.ui.Thousandrows をちょっとだけ紹介させてください

まず、1つめを紹介する前に。

僕の作った、Thousandrows について紹介させてください。

Page 61: goog.ui.Component のはぐれかた

• 大量の行を、すぐ表示• つなぎ目なしでスクロール(≠ページング)• 任意の箇所にジャンプ(≠単なる無限スクロール)

大量の行を効率よく表示できる、UIコンポーネントです。

Page 63: goog.ui.Component のはぐれかた

Thousandrows

Page

Row

コンポーネントの親子関係を築いているので、rowsのdisposeも効率的にでき、インスタンス間通信もしやすく、Thousandrowsが一体となって機能することができます。

Page 64: goog.ui.Component のはぐれかた

goog.ui.Component↓

goog.ui.Control

↓goog.ui.Scroller

↓goog.ui.Thousandrows

このThousandrows を作るために、まずScrollerを作りました。継承させています。

Page 65: goog.ui.Component のはぐれかた

Scroller

Slider

Scrollerは、こういう構造になっています。

ですが、Thousandrowsのようなものを作れるように、Sliderはchildrenに加えるわけにはいきません。

Page 66: goog.ui.Component のはぐれかた

ケース1:関連するインスタンスを、childに加えられないことがある

Page 67: goog.ui.Component のはぐれかた

goog.ui.Componentの children は、一律で contentElement_ にappendされるべき

Scroller というからには、childをcontentElement に入れていけるべきです。

Page 68: goog.ui.Component のはぐれかた

Scroller

Slider

contentElement_

Page 69: goog.ui.Component のはぐれかた

Scroller

Slider

Scrollerのchildにできない

contentElement_

child は、ユーザーが加えていけるようにするため、空にしておく必要がありました。

Page 70: goog.ui.Component のはぐれかた

• childたちは、contentElement_ に並ぶようにしておかないと、 addChildAt が動かなくなる。(child.getElement()のsiblingに挿入してる)

•異質のcomponentインスタンスをchildrenに混ぜると、this.forEachChild しにくくなるのでおすすめできない。

Slider を、Scroller のchild にできない理由。

Page 71: goog.ui.Component のはぐれかた

Scroller に関連する Slider コンポーネントを、child にできない。

childにできないことがわかった。こんなときは、どうするか?

Page 72: goog.ui.Component のはぐれかた

childにせず、手動で親のライフサイクルに追従させればOK

childにせず、手動で親のライフサイクルに追従させるという手があります。

Page 73: goog.ui.Component のはぐれかた

/** @constructor */goog.ui.Scroller = function () { goog.base(this); (‘...’);

this.slider_ = new goog.ui.Slider();};

(‘...’);

通常と同じ

Page 74: goog.ui.Component のはぐれかた

goog.ui.Scroller.prototype.createDom = function () { (‘...’);

this.slider_.createDom(); this.getElement().appendChild(this.slider_.getElement());};

通常と同じ

Page 75: goog.ui.Component のはぐれかた

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 することも考えられますが、こうするとイベントがパブリックになります。

Page 76: goog.ui.Component のはぐれかた

goog.ui.Scroller.prototype.exitDocument = function () { (‘...’);

this.slider_.exitDocument();  // なくてもいい};

通常と違う

Page 77: goog.ui.Component のはぐれかた

goog.ui.Scroller.prototype.disposeInternal = function () { if (this.slider_) { this.slider_.dispose(); this.slider_ = null; }

(‘...’);};

通常と違う

Page 78: goog.ui.Component のはぐれかた

ケース1まとめ:childにできない場合、手動で状態を変えていくことで

インスタンスを管理しつづけられる。

Page 79: goog.ui.Component のはぐれかた

ケース2:childを、parnetの contentElement_ 以外に

appendしたいとき

Page 80: goog.ui.Component のはぐれかた

再び、ちょっと紹介させてください

Page 81: goog.ui.Component のはぐれかた

タブ

Page 82: goog.ui.Component のはぐれかた

tab tab tab

frame

tabs

タブのchild は、frameひとつです。でも・・・

Page 83: goog.ui.Component のはぐれかた

frame

childにしたい。。でもDOM的にとても距離がある

tab tab tabtabs

距離がある。tab のDOMにappend することは、考えられない。

Page 84: goog.ui.Component のはぐれかた

append先だけ、変えればOK.

Page 85: goog.ui.Component のはぐれかた

/** @constructor */app.ui.Tab = function () { goog.base(this);

this.frame_ = new app.ui.Frame();};

(‘...’);

通常と同じ

まず、親のコンストラクタで、子のインスタンスを生成しておきます。

Page 86: goog.ui.Component のはぐれかた

app.ui.Tab.prototype.createDom = function () {

(‘...’);

this.frame_.createDom();

// 別のDOMに入れても問題ない。

dh.appendChild(

App.getInstance().getFrameWrapEl(),

this.frame_.getElement());

};

通常と違う

次に、親のcreateDom内で、子も一緒に

Page 87: goog.ui.Component のはぐれかた

• childを、遠くのDOMにappendしても、特に問題ない• parentEventTarget として機能、disposeなども• getContentElement は、通ったらだめ。(addChildAt も)

注意点。

Page 88: goog.ui.Component のはぐれかた

ケース2まとめ:DOM的関係性があまりなくても、

親子関係は築ける。

Page 89: goog.ui.Component のはぐれかた

ケース3:ライフサイクルを意図的にずらしたいとき

Page 90: goog.ui.Component のはぐれかた

先ほどの tab と frame の例は忘れて頂いて・・・

ここ。ちょっと雑です。具体例の使いかたが雑なので注意してください。

Page 91: goog.ui.Component のはぐれかた

タブ

もういちど出て来ました。さっきの例は忘れてください。

Page 92: goog.ui.Component のはぐれかた

非選択タブ。frameは、まだレンダリングしてない

タブ自身は、初期表示時にすべてレンダリングされますが、非選択のタブのフレームは、まだレンダリングしたくない。

Page 93: goog.ui.Component のはぐれかた

親が選択されたときに、子をレンダリングしたい。

(意図的にライフサイクルをずらしたい)

Page 94: goog.ui.Component のはぐれかた

/** @constructor */app.ui.Tab = function () { goog.base(this);

this.frame_ = new app.ui.Frame();};

(‘...’);

通常と同じ

まず、親のコンストラクタで、子のインスタンスを生成しておきます。

Page 95: goog.ui.Component のはぐれかた

app.ui.Tab.prototype.createDom = function () {

(‘...’);

// frame はまだレンダリングしない。appendもされない。

};

通常と違う

createDom で、Tabのelement は作る。frame のelement は作らない。まだいらないから。

Page 96: goog.ui.Component のはぐれかた

app.controller.Tab.prototype.processSelected = function () {

(‘...’);

if (!this.frame_.isInDocument()) {

this.frame_.render(wrapEl);

}

};

通常と違う

・・・タブである自分が選択されたタイミングで、初めてframeをレンダリングします。

append先は、前述したとおり、自由な場所を指定できます。

childにしてあるので、disposeなどは、親と同じときに自動で処理されます。

Page 97: goog.ui.Component のはぐれかた

ケース3まとめ:必要に応じて、ライフサイクルを

ずらすことも可能。

Page 98: goog.ui.Component のはぐれかた

• ケース1まとめ:childにできない場合、手動で状態を変えていくことでインスタンスを管理しつづけられる。

• ケース2まとめ:DOM的関係性があまりなくても、親子関係は築ける。

• ケース3まとめ:必要に応じて、ライフサイクルをずらすことも可能。

Page 99: goog.ui.Component のはぐれかた

ツリー化のメリットと基本のライフサイクルを理解したうえで、柔軟にClosure Libraryからはぐれましょう!

Page 100: goog.ui.Component のはぐれかた

おわり