Backbone.js Advent Calendar 2012 – 03日目

最近になってようやくBackbone.jsを触り始めた高梨ギンペイです。

まだよくわかってないけど、初めて触ってみて気付いた事のうち、Backbone.jsが自動的に処理してくれる部分について書いてみました。

  • Viewの要素生成の仕組み
  • Viewのイベント監視の仕組み

Backbone.jsのバージョンは0.9.2です。

内部用ユーティリティ

ローカルスコープの汎用関数。方々で使ってるので、先にこれ書いておきます。(この項は後から見返せばいいです。)

getValue(object, key)

object[key]が関数なら実行して戻り値、そうでなければプロパティの値を返すだけです。

要素の生成

properties.elがview.elにならない……

サンプルを見てみると、Backbone.View.extend(properties)のproperties.elにはセレクターを与えてるのが多いです。でもこれ、インスタンスのプロパティview.elとは同じになりません。え、何これ??

var MyView = Backbone.View.extend({
  el: 'body'
});
var view = new MyView();
console.log(view.el == 'body');  // => false
console.log(view.el);  // => <body>

セレクターの文字列だったのに、要素(DOMノード)になっちゃってます。なんじゃこりゃ!

view.elをつくる仕組み

中のコードを見てみます。どうやらsetElementというメソッドで処理されてるみたいです。

elementにはthis.elであったものが格納されています。

      this.$el = (element instanceof $) ? element : $(element);
      this.el = this.$el[0];

ああーなるほどなるほど。コードを読むと一発ですね。

つまりthis.$el = $(properties.el)のように処理されてます。という事はelに与えるのはセレクターでも何でも良んですね。

で、その後でthis.el = this.$el[0]なので、DOMノードになると。

なるほどー。

tagName等での指定も

前項のようにelが指定されていればそれを使います。未指定の場合は、tagName, className, id, attributesといったプロパティから生成されます。tagNameを省略すると"div"に。他は、まあわかりますかね。

    // Ensure that the View has a DOM element to render into.
    // If `this.el` is a string, pass it through `$()`, take the first
    // matching element, and re-assign it to `el`. Otherwise, create
    // an element from the `id`, `className` and `tagName` properties.
    _ensureElement: function() {
      if (!this.el) {
        var attrs = getValue(this, 'attributes') || {};
        if (this.id) attrs.id = this.id;
        if (this.className) attrs['class'] = this.className;
        this.setElement(this.make(this.tagName, attrs), false);
      } else {
        this.setElement(this.el, false);
      }
    }

処理内容:

  1. if: properties.elがない?
    1. 属性マップ取得
    2. IDが指定されていれば属性マップに追加
    3. クラスが指定されていれば属性マップに追加
    4. properties.tagNameと属性マップから要素を生成
    5. 生成した要素をプロパティに設定
  2. else
    1. properties.elをプロパティに設定

makeは普通のview.makeみたい。

ifの書き方から察するに、tagNameを使うのが一般的なパターンなのかな。まあそうか、普通はドキュメントツリーから独立した要素を生成するか。

タイミング

この関数はコンストラクタの中で呼ばれてます。なので最初からview.elは$オブジェクトになってます。

イベント

view.events

イベントの種類とセレクター、リスナーとなるメソッド名を与えると、クリックとかのイベント発火時にメソッドを呼んでくれます。これも登録処理が自動的に行われているので、Backboneの利用者はeventsにテキストでぽちぽち書くだけです。(イベント発火時の処理は普通にメソッドで書くけど。)

処理

view.delegateEvents()で登録してるみたいです。

    // Set callbacks, where `this.events` is a hash of
    //
    // *{"event selector": "callback"}*
    //
    //     {
    //       'mousedown .title':  'edit',
    //       'click .button':     'save'
    //       'click .open':       function(e) { ... }
    //     }
    //
    // pairs. Callbacks will be bound to the view, with `this` set properly.
    // Uses event delegation for efficiency.
    // Omitting the selector binds the event to `this.el`.
    // This only works for delegate-able events: not `focus`, `blur`, and
    // not `change`, `submit`, and `reset` in Internet Explorer.
    delegateEvents: function(events) {
      if (!(events || (events = getValue(this, 'events')))) return;
      this.undelegateEvents();
      for (var key in events) {
        var method = events[key];
        if (!_.isFunction(method)) method = this[events[key]];
        if (!method) throw new Error('Method "' + events[key] + '" does not exist');
        var match = key.match(delegateEventSplitter);
        var eventName = match[1], selector = match[2];
        method = _.bind(method, this);
        eventName += '.delegateEvents' + this.cid;
        if (selector === '') {
          this.$el.bind(eventName, method);
        } else {
          this.$el.delegate(selector, eventName, method);
        }
      }
    },

長いけど半分はコメントなので大丈夫。

こんな流れです:

  1. if: eventsが未指定
    1. return
  2. 既存のリスナーを削除
  3. for: 各イベント
    1. eventsからリスナーを取得
    2. if: リスナーが関数じゃない?
      1. インスタンスのメソッドをリスナーに
    3. if: リスナーがない?
      1. throw: メソッドないよ
    4. リスナーをインスタンスにバインド
    5. if: セレクターが空?
      1. this.$elにバインド
    6. else:
      1. this.$el内のセレクターに合致するものにバインド

要素へのリスナーの登録はjQueryの .bind()と .delegate()を使ってます。これちょっと古い書き方ですね。jQuery 1.7以降では .on()に統一されてます。model.bind()はmodel.on()になってるのに……。

タイミング

この関数はコンストラクタの中で呼ばれてます。というか要素 (view.$el) を作るときですね。

自分で関数を呼ぶ事も可能です。

というわけで、基本的にはview.render()の中でthis.$el.html()してる分には良いのですが、this.$el = $newElementとかやっちゃうとアウト。自前で .delegateEvents()と .undelegateEvents()を実行しないとイベントが動かない。

ソースコード読むの楽しい

全体で1,400行、コメントと空行を除くと900行を切ります。さらさらーっと目を通してみると楽しそうです。

ちゃんちゃん。