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行を切ります。さらさらーっと目を通してみると楽しそうです。
ちゃんちゃん。