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); } }
処理内容:
- if:
properties.el
がない?- 属性マップ取得
- IDが指定されていれば属性マップに追加
- クラスが指定されていれば属性マップに追加
properties.tagName
と属性マップから要素を生成- 生成した要素をプロパティに設定
- else
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); } } },
長いけど半分はコメントなので大丈夫。
こんな流れです:
- if:
events
が未指定- return
- 既存のリスナーを削除
- for: 各イベント
events
からリスナーを取得- if: リスナーが関数じゃない?
- インスタンスのメソッドをリスナーに
- if: リスナーがない?
- throw: メソッドないよ
- リスナーをインスタンスにバインド
- if: セレクターが空?
this.$el
にバインド
- else:
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行を切ります。さらさらーっと目を通してみると楽しそうです。
ちゃんちゃん。