goog.ui.Componentのはぐれ方
  アリエル・ネットワーク開発部 高村
私
 高村壮一(@stakamur)
HTMLとCSSをひたすら書く
          ↓
        jQuery
          ↓
     vimとのであい
         ↓
   Closure Library
         ↓
こないだから Sencha Touch ..

開発部UIチームで働いています
github projects
piglovesyou / flickGal (jQuery)
piglovesyou / closure-scroller
piglovesyou / 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




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




むしろ、Closure LibraryにはaddChildなどのメソッドがあることから、奨励していると思う
http://tiny-word.appspot.com


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

コンポーネントのツリー化を基礎に設計されています。
ツリー化は、基本。
(例外はやまほどあるだろう)
しかし、ツリー構造を作りにくいときも・・




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

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

そこで、今日お話したいのは、・・・
前半:なぜ、goog.ui.Componentを   後半
   ツリー化すべきなのか?
tを   後半: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
                             ↓
 • DOM Exception を防ぐ   enterDocument
 • enter したときだけ
                             ↓
   listener を持たせられる
   (exit したら外せる)        exitDocument
                             ↓
                           dispose




このステップを踏むことで、ブラウザ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です。

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




       capture




                           child.dispatchEvent()


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、
         createDom
                                手動      親のelementにappend
              ↓
       enterDocument                       enterDocument
                                自動
              ↓
        exitDocument                       enterDocument
                                自動
              ↓
           dispose                             dispose
                                自動


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


                         コンストラクタ                  コン

                              ↓
                                                      c
     parent.render()      createDom
                                                 親の
      (bodyに、1度だけ
                              ↓
       appendされる)
                         enterDocument                en
                              ↓
                         exitDocument                 en
                              ↓
     parent.dispose()
                            dispose



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

したがって、子は基本的にrenderメソッドを使わない、といえると思います。(もちろん、使うこともでき
る)
理由その3:
          親のcreateDom → 子のcreateDom で、
             安全にDOMを組み立てられ、
          効率よくappendすることができます。




その3まとめ。
parent.addChild(child, true); を使えばいい

(       )
  という方もいるかも知れませんが、
      基本形はこの形で考えます
理由その1:インスタンスのdisposeが、大変になる。


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


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




以上3点が、「なぜツリー化するのか」の理由です。
ツリー化するメリットが
理解いただけたかと思います
前半:なぜ、goog.ui.Componentを   後半:
             ツリー化すべきなのか?




十分理解していただいたところで。
tを     後半:goog.ui.Componentのはぐれかた
?            ツリー化する上での例外ケース3種類




後半は、この基本形からそれなければいけないケースを、3例、紹介したいと思います。
ツリーの基本形からはぐれる、
 3つのケースを紹介します
goog.ui.Thousandrows を
           ちょっとだけ紹介させてください




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

僕の作った、Thousandrows について紹介させてください。
• 大量の行を、すぐ表示
   • つなぎ目なしでスクロール(≠ページング)
   • 任意の箇所にジャンプ(≠単なる無限スクロール)




大量の行を効率よく表示できる、UIコンポーネントです。
http://stakam.net/closure/120722/
Page




                                      Row

Thousandrows




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

                             ↓
                      goog.ui.Scroller
                             ↓
                   goog.ui.Thousandrows




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

   Scroller




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

ですが、Thousandrowsのようなものを作れるように、Sliderはchildrenに加えるわけにはいきません。
ケース1:関連するインスタンスを、
 childに加えられないことがある
goog.ui.Componentの children は、
       一律で contentElement_ にappendされるべき




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




                  Slider

 Scroller
contentElement_




                                             Slider

   Scroller

                      Scrollerのchildにできない


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したいとき
再び、ちょっと紹介させてください
タブ
tabs
         tab         tab              tab




                              frame




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



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



                          frame




距離がある。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からはぐれましょう!
おわり

goog.ui.Component のはぐれかた