Backbone.jsのソースコードを読んでみる
今回はBackboneのクラス生成など、主にセットアップに関するところ
対象Version
Backbone.js 1.2.3
実行環境周り
(function(factory) {
var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global);
// |
// ---------- ブラウザ環境ではwindowオブジェクト
// Node環境ではglobalオブジェクト
// 以後root.hogehogeと出てきたらwindow.hogehogeなどと読み替えれば問題無いです
// スクリプトの実行方法をAMD/Commonjs/scriptタグ形式の3つのパターンに適用させている
// ここはAMDの場合のパターンである
if (typeof define === 'function' && define.amd) {
define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
// Export global even in AMD case in case this script is loaded with
// others that may still expect a global Backbone.
root.Backbone = factory(root, exports, _, $);
});
// ここはCommonjsのパターンである
} else if (typeof exports !== 'undefined') {
var _ = require('underscore'), $;
try { $ = require('jquery'); } catch(e) {}
factory(root, exports, _, $);
// ここはscriptタグのパターンである
} else {
root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
}
}(function(root, Backbone, _, $) {
// ここの段階ではまだBackboneと名前のついた参照は空です。
// 以下で続くコードによってBackboneと名前のついた参照に対してメンバを刺していきます
.....
}));
- Backboneの実行環境に応じて、ブラウザならrootはwindow, Nodeならglobalの参照を持つようになっている。
- モジュールの管理手法に対しても色々なパターンをサポートしている。
- jQueryが一番優先されてしまうので注意する必要があります。(jQuery, Zepto)の両方がrootに刺さっている場合はjQueryを使ってしまう。
セットアップ
// Backbone.noConflictにおいてすでに存在していたBackboneのオブジェクトを参照することができるようになります
// これはすでにglobalの領域にBackboneが存在した際に、今回生成したのBackboneはなく、今回生成するBackboneの1つ前に存在していたBackboneの参照を見ることができるようになります
var previousBackbone = root.Backbone;
// Arrayではないオブジェクトに対してArray.sliceと同様の振る舞いをさせているところがいくつかあるがそれのショートカットのためにメソッドをキャッシュしているだけ
var slice = Array.prototype.slice;
Backbone.VERSION = '1.2.3';
Backbone.$ = $;
Backbone.noConflict = function() {
root.Backbone = previousBackbone;
return this;
};
/////////////////////////////////////////////////////////////////////////////////////////////////
// この辺はレガシーなWEBサーバーに対して、Backbone.syncでHTTPリクエストする際にリクエストの内容を補足するための仕組みである
// あまり使うことはなさそうな気がする
/////////////////////////////////////////////////////////////////////////////////////////////////
// trueにすることでPATCH/PUT/DELETEのメソッドのリクエストをPOSTに変換することができる
Backbone.emulateHTTP = false;
// trueにすることでHTTPリクエストのcontentTypeを'application/x-www-form-urlencoded'に変換することができる
Backbone.emulateJSON = false;
underscore.jsのAPIを盛り込むための仕組み
/////////////////////////////////////////////////////////////////////////////////////////////////
// Backbone.XXXに対してunderscore.jsのメソッドを使えるようにするための拡張ができるようにするための
// 仕組みを定義している
// この仕組のおかげでModel.underscoreFunctionが使えるんですね。
// 以下がそのサンプルであるがundersore.jsの機能の一部をmixinという形でBackbone.XXXオブジェクトに対して生やしている
// 例えば、Backbone.Modelに対してはこんなの
// var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
// omit: 0, chain: 1, isEmpty: 1 };
// addUnderscoreMethods(Model, modelMethods, 'attributes');
/////////////////////////////////////////////////////////////////////////////////////////////////
var addMethod = function(length, method, attribute) {
// undersocreのメソッド名とそのメソッドの引数の数から、引数の役割まで決定してしまっているので
// ちょっと危険だけど、underscore側がそのへんが統一された設計になっているので何とか動いているんですね
// ないとは思うけど、underscore以外の何かしらのライブラリのやくわりをもたせるようなことがある場合にこの考え方は破錠するんでしょうね
// Backbone.jsとunderscore.jsの作者が同じだからこそでできているんでしょうが
//
// ちなみにやろうと思えばすべてdefault:句の実装だけで足りるはずだけど、apply使わないほうが実行が早い(のかな)ということでこんな実装になっている。
// これと同じような実装は他にもみられる
switch (length) {
case 1: return function() {
return _[method](this[attribute]);
};
case 2: return function(value) {
return _[method](this[attribute], value);
};
case 3: return function(iteratee, context) {
return _[method](this[attribute], cb(iteratee, this), context);
};
case 4: return function(iteratee, defaultVal, context) {
return _[method](this[attribute], cb(iteratee, this), defaultVal, context);
};
default: return function() {
var args = slice.call(arguments);
args.unshift(this[attribute]);
return _[method].apply(_, args);
};
}
};
var addUnderscoreMethods = function(Class, methods, attribute) {
_.each(methods, function(length, method) {
if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
});
};
var cb = function(iteratee, instance) {
if (_.isFunction(iteratee)) return iteratee;
if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee);
if (_.isString(iteratee)) return function(model) { return model.get(iteratee); };
return iteratee;
};
var modelMatcher = function(attrs) {
// _.matchesを初めてこれで知りましたが、便利そうでした。
var matcher = _.matches(attrs);
return function(model) {
return matcher(model.attributes);
};
};
- Backbone.XXXが中途半端にuserscore.jsのAPIを使えるようになっているのはこの仕組のおかげ
- applyとapply使わないのどれくらい実行コストに違いがあるのかが気になる