Backbone.View。

Backboneの中の登場人物としては、サッカーで言うところのフォワード、、、なのかな。いわゆる「ユーザーインターフェイス」の役割を担うわけですから、まあ、最前線ですよね。「ユーザーインターフェイス」、というからには何かと何かの橋渡しをするのですが、それは「ユーザーの動作」と「モデルオブジェクト(or コレクションオブジェクト)」との橋渡し、ということになります。

もうちょっと具体的に言い換えてみると・・・:

  • 【役割1】ユーザーからのイベント(クリックとかキー入力、etc…)を受け取り、それ対して適切な指示をモデルオブジェクト(orコレクションオブジェクト)に送る
  • 【役割2】モデルオブジェクト(とそのコレクションオブジェクト)の動作(オブジェクトの変更・削除・追加、etc…)に応じて、適切な描画をブラウザに指示する

と考えてみます。ここでキーワードは:

  • 「ユーザーイベント」
  • 「モデルイベント」(もしくはコレクションイベント)
  • 「描画」

の3つに絞ることができそうです。ビュー同士のコミュニケーション(たとえばメインビューがサブビューに描画の指示をだす、など)は、ビューの本来的な動きではなく、ビューの役割をアプリケーションの中で分担するための動きと考えることにします。つまり「イベントの処理」と「描画の処理」がとても重要になってくると考えられます。

では、いったいどんなコンストラクターとプロトタイプが実装されているのでしょうか?ソースコードを追いながら、フレームワークの使い手である我々が、どのようにBackbone.Viewを使えばいいのか、考えてみたいと思います。すごく短いコードなので、僕でも理解できそうだったので。注釈付きの親切なソースは、こちらから見れます。

まず、コンストラクターをみてみましょう。

#javascript
var View = Backbone.View = function(options) {
    this.cid = _.uniqueId('view');
    _.extend(this, _.pick(options, viewOptions));
    this._ensureElement();
    this.initialize.apply(this, arguments);
  };

「オブジェクトにユニークなid(cid)を与え、与えられたオプションを身につけ、initialize()を実行する」と書いてあります、、、が、1つ大事な処理を内部で行ってますね。this._ensureElement();がそれです。注釈を読むと「初期化時にDOMに結びついていない場合にはDOMツリーの外にオブジェクトを生成する」という感じですかね。_ensureElement()のコードを追ってみると「tagNameプロパティからdocument.createElementして、DOMエレメントオブジェクトを生成して、それをsetElementする、elプロパティが与えられている場合はそれをそのままsetElementする」と言えそうです。setElement()はパブリックなメソッドですが、実はコンストラクタも内部的に呼び出しているのですね。

では、setElement()は何をするメソッドなんでしょうか?注釈には「ビューのエレメント(elプロパティ)を変更し、ビューのイベントを新しいエレメントに委譲し直す」と書かれています。ソースを見てみましょう。

#javascript 
    setElement: function(element) {
      this.undelegateEvents();
      this._setElement(element);
      this.delegateEvents();
      return this;
    },
#javascript 
   _setElement: function(el) {
      this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
      this.el = this.$el[0];
    },
#javascript
    undelegateEvents: function() {
      if (this.$el) this.$el.off('.delegateEvents' + this.cid);
      return this;
    },
 #javascript
   delegateEvents: function(events) {
      events || (events = _.result(this, 'events'));
      if (!events) return this;
      this.undelegateEvents();
      for (var key in events) {
        var method = events[key];
        if (!_.isFunction(method)) method = this[method];
        if (!method) continue;
        var match = key.match(delegateEventSplitter);
        this.delegate(match[1], match[2], _.bind(method, this));
      }
      return this;
    },

「与えられたエレメントを、jQueryでラップして$elプロパティにアサインする。また、$elの先頭要素をelプロパティにアサインする。だたし、すでに$elがアサイン済みの場合は、一旦それに委譲されているイベントを全てオフにし、新しくアサインされたエレメントに委譲し直す」と、言葉に翻訳できそうです。

つまり、viewオブジェクトに何かしらエレメントをセットする時(言い換えると、初期化時、もしくはsetElement()を起動する時)には、前にセットされてたエレメントにバインドされてるコールバックを全てアンバインドして(初期化時は関係ない)、もう一度新しいエレメントにバインドし直す、という動作をしていることがわかります。

jeremyはここで「委譲(delegate)」という言葉を使っていますが、これは「viewオブジェクトに定義されたイベントの処理を、jQueryオブジェクトのonメソッドに委譲する」という意味の「委譲」ということといえそうです。実際にdelegateメソッドのソースを覗いてみると$elオブジェクトのonメソッドを呼び出していることがわかります。

 #javascript
   delegate: function(eventName, selector, listener) {
      this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
      return this;
    },

マニュアルを読むと「By default, delegateEvents is called within the View’s constructor for you, so if you have a simple events hash, all of your DOM events will always already be connected, and you will never have to call this function yourself. 」とあります。つまり、「初期化時に自動的に呼び出してくれるので、自分で呼び出す必要はないよ」といっています。ここまできて、【役割1】はeventsハッシュ(と、viewオブジェクトに結びついたコールバック関数)を定義すれば、あとは自動的にBackbone.Viewが処理してくれることで果たせそうです。また、viewに紐付いているDOMエレメントそのものを変更する場合も、setElementメソッドを使えば、適切にコールバックをはバインドし直してくれるのでgoodですね。

次にrenderメソッドを見てみましょう。

#javascript
    render: function() {
      return this;
    },

そうですね、空っぽです。注釈には「適切なhtmlをthis.elに埋める(populate)ためにオーバーライドしてね」とかいてあります。また、renderと対になるのがremoveです。こちらは「this.elをDOMツリーの外に出し、stopListenする」と書かれています。また、renderも、removeも自分自身を返しています(実はsetElementやdelegateEventsも自分自身を返しています)。なにかしらチェイナブルな実装を期待しているようです。
こういうことでしょうか。アプリケーション内のview同士は、setElement, render, remove(あるいはdelegateEventsも)のインターフェイスを介して協調動作させることができる、それぞれのインターフェースは内部処理を行った後、自分自身を返すので、さらに数珠繋ぎ的に処理を続けることができる、と。

、、、これでソースコードに書かれていることは全てです。ほんとにシンプルですね。
余談ですが、Jeremyはマニュアルの冒頭に書いています:

Philosophically, Backbone is an attempt to discover the minimal set of data-structuring (models and collections) and user interface (views and URLs) primitives that are generally useful when building web applications with JavaScript.

「Backboneは、JavaScriptによるウェブアプリケーションを構築する際の、データ構造とユーザーインターフェースのミニマルセットを見つけるための試みである」
まさにミニマルだなと、思います。ソースを読むだけで、余計なものがまったくないとわかります。

さて、【役割2】はどのようにして果たせばよいでしょうか。これにはBackbone.Eventモジュールが役に立ちます。実はBackbone.ViewはBackbone.Eventモジュールをmix-inしている(=Backbone.ViewのプロトタイプオブジェクトをBackbone.Eventオブジェクトで拡張している)ので、「他のオブジェクトの動きに応じた動作をさせる」ことが、デフォルトで可能です。実はBackbone.ModelオブジェクトもBackbone.Collectionオブジェクトも同じくデフォルトでBackbone.Eventモジュールをmix-inしています。Backbone.Eventモジュールをmix-inすると、「自分に向けられた動作に応じた動き」ではなく、他人の動作に応じた動きをオブジェクトに定義できるのです。コレがBackbone.Eventの特徴と言えるでしょう。具体的にはlistenToメソッドが実現しています。
構文は「this.listenTo other event(s) callback」というものです。コレを使えば、ビューオブジェクトはアプリケーション内の任意のオブジェクトの動作に応じた描画を行うことができます。構文は「view.listenTo someModel(or collection) event(s) view.render」という感じになります。initialize時にこれを実装しておけば、アプリケーションの内部状態の変更を自動的に描画に反映することができます。【役割2】はこれでOKです。

ちなみに、今述べた協調関係はView対モデル(orコレクション)の協調関係ですが、View対Viewの協調関係もlistenToで築けるのか?でも、それはあまり良くないだろうなと僕は考えます。listenToはデータストラクチャーの変更を意図して設計されたメソッドと僕は考えるからです。Viewはデータストラクチャーを表現していません彼らはユーザーインターフェースを表現しているのですから。

具体例を書かずにここまで来てしまいました。Jeremyも公式サイトで簡単なToDoアプリのチュートリアル以外は具体例を挙げていません。でも、それでいいんじゃないかと思います。「ミニマルセット」を追求した結果のBackboneなわけですから、後は開発者の想像力と構築力の問題になるのだと思います。いかようにでも作れる、でも、もっとも必要なモノがそこにある、それがBackboneフレームワークだと捉えられそうです。