Backbone 做者在源碼中作了很好的註釋,這裏只是錦上添花,補充一些我的的理解而已。javascript
// Backbone.js 1.2.3 // (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Backbone may be freely distributed under the MIT license. // For all details and documentation: // http://backbonejs.org (function(factory) { // Establish the root object, `window` (`self`) in the browser, or `global` on the server. // We use `self` instead of `window` for `WebWorker` support. var root = (typeof self == 'object' && self.self == self && self) || (typeof global == 'object' && global.global == global && global); // Set up Backbone appropriately for the environment. Start with 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, _, $); }); // Next for Node.js or CommonJS. jQuery may not be needed as a module. } else if (typeof exports !== 'undefined') { var _ = require('underscore'), $; try { $ = require('jquery'); } catch(e) {} factory(root, exports, _, $); // Finally, as a browser global. } else { root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); } }(function(root, Backbone, _, $) { // Initial Setup // ------------- // Save the previous value of the `Backbone` variable, so that it can be // restored later on, if `noConflict` is used. var previousBackbone = root.Backbone; // Create a local reference to a common array method we'll want to use later. var slice = Array.prototype.slice; // Current version of the library. Keep in sync with `package.json`. Backbone.VERSION = '1.2.3'; // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns // the `$` variable. Backbone.$ = $; // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable // to its previous owner. Returns a reference to this Backbone object. Backbone.noConflict = function() { root.Backbone = previousBackbone; return this; }; // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and // set a `X-Http-Method-Override` header. Backbone.emulateHTTP = false; // Turn on `emulateJSON` to support legacy servers that can't deal with direct // `application/json` requests ... this will encode the body as // `application/x-www-form-urlencoded` instead and will send the model in a // form param named `model`. Backbone.emulateJSON = false; // Proxy Backbone class methods to Underscore functions, wrapping the model's // `attributes` object or collection's `models` array behind the scenes. // // collection.filter(function(model) { return model.get('age') > 10 }); // collection.each(this.addView); // // `Function#apply` can be slow so we use the method's arg count, if we know it. var addMethod = function(length, method, attribute) { 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); }; } }; // 添加 underscore 方法 var addUnderscoreMethods = function(Class, methods, attribute) { _.each(methods, function(length, method) { if (_[method]) Class.prototype[method] = addMethod(length, method, attribute); }); }; // Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`. 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) { var matcher = _.matches(attrs); return function(model) { return matcher(model.attributes); }; }; // Backbone.Events - 事件 // ------------------------- // A module that can be mixed in to *any object* in order to provide it with // a custom event channel. You may bind a callback to an event with `on` or // remove with `off`; `trigger`-ing an event fires all callbacks in // succession. // // var object = {}; // _.extend(object, Backbone.Events); // object.on('expand', function(){ alert('expanded'); }); // object.trigger('expand'); // // Events 全部事件行爲都保存在 obj._events 屬性中, // 不管是 on 仍是 listenTo,事件行爲都是保存在事件觸發者身上, // listenTo 其實只是 on 的另外一種調用形式。 // 因此,當 events.off() 方法會解綁全部事件,同時也會解除全部被監聽關係。(即監聽者沒法再繼續監聽) // events.stopListening() 中止監聽全部事件,從被監聽者身上解除事件處理。 // // obj._listeningTo 是保存對被監聽對象的引用。 // obj._listenId 是每一個事件觸發者本身身份的 ID,當本身被其餘人監聽時,用以標識本身身份。 // obj._listeners 是全部對本身進行監聽的對象引用映射。 // // Backbone.Events 的 listenTo 與 stopListening 方法實現原理: // 例如監聽者 listener 和被監聽者 listenee。 // // `listener.listenTo(listenee, 'any', callback);` // // 整個監聽過程大體能夠分爲三個部分: // 1. 生成監聽關係表: // // ``` // { // count: Int, // 監聽次數,當 count 爲 0 時,表示兩者再也不存在任何監聽關係,從雙方刪除監聽關係。 // id: String, // listener._listenId,標識監聽者身份。 // listeningTo: Object, // listener._listeningTo,保存全部監聽關係表(以被監聽者 ID 做爲主鍵) // obj: listenee, // listenee,被監聽者。 // objId: String // listenee._listenId,被監聽者 ID。 // } // ``` // // 2. 在 listenee 中保存事件以及事件回調,即在 listenee._events['any'] 隊列中推入事件處理關係表。 // // ``` // { // callback: Function, // 回調函數 // context: Object, // ctx: Object, // listening: Object // listening 就是監聽關係表 // } // ``` // // 3. 在雙方各自添加監聽關係: // 在 listener 方面,以 listenee._listenId 爲主鍵,保存在 listener._listeningTo 字段。 // 在 listenee 方面,以 listener._listenId 爲主鍵,保存在 listenee._listeners 字段。 // 即如下等式是成立的 `listener._listeningTo[listenee._listenId] === listenee._listeners[listener._listenId]`。 // // `listener.stopListening(listenee, 'any', callback);` // // 中止監聽過程實際上是經過 `listenee.off('any', callback, listener)` 來實現,這一過程包括: // 從 listenee._events 移除事件處理關係表。 // 經過監聽關係表裏的 count 字段減一併判斷是否爲 0,從而決定是否要從雙方移除監聽關係。 var Events = Backbone.Events = {}; // Regular expression used to split event strings. // 正則表達式:多個事件名以空格分隔。 var eventSplitter = /\s+/; // Iterates over the standard `event, callback` (as well as the fancy multiple // space-separated events `"change blur", callback` and jQuery-style event // maps `{event: callback}`). // // 遍歷定義的事件。 // // iteratee 是迭代函數,即 onApi, offApi, triggerApi, onceMap 函數 // eventsApi 做用是將 events, name, callback, opts 參數整理成標準格式傳遞給 iteratee 調用。 var eventsApi = function(iteratee, events, name, callback, opts) { var i = 0, names; if (name && typeof name === 'object') { // Handle event maps. if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback; for (names = _.keys(name); i < names.length ; i++) { events = eventsApi(iteratee, events, names[i], name[names[i]], opts); } } else if (name && eventSplitter.test(name)) { // Handle space separated event names by delegating them individually. for (names = name.split(eventSplitter); i < names.length; i++) { events = iteratee(events, names[i], callback, opts); } } else { // Finally, standard events. // events 事件映射表; // name 事件名稱; // callback 事件處理函數; // opts 額外參數 events = iteratee(events, name, callback, opts); } return events; }; // Bind an event to a `callback` function. Passing `"all"` will bind // the callback to all events fired. // 綁定事件 Events.on = function(name, callback, context) { return internalOn(this, name, callback, context); }; // Guard the `listening` argument from the public API. // 內部綁定事件函數,它是 Events.on, Events.listenTo 公開 API 內部真正實現事件綁定的函數 // 所以它比 Events.on, Events.listenTo 多一個參數 listening, // 該參數爲真,表示它正實現 listenTo 方法;不然表示它正式實現 on 方法。 var internalOn = function(obj, name, callback, context, listening) { // 執行 eventsApi obj._events = eventsApi(onApi, obj._events || {}, name, callback, { context: context, ctx: obj, listening: listening }); // 若是當前是實現 listenTo 方法,須要在被監聽者的 _listeners 中,添加監聽者的引用關係。 if (listening) { var listeners = obj._listeners || (obj._listeners = {}); // listening.id 是監聽者的 _listenId。 listeners[listening.id] = listening; } return obj; }; // Inversion-of-control versions of `on`. Tell *this* object to listen to // an event in another object... keeping track of what it's listening to // for easier unbinding later. // Events.on 操做的逆操做,表示監聽另外一個對象的事件,並保持對該對象的引用,以便解綁事件。 Events.listenTo = function(obj, name, callback) { // 若是 obj 爲否,則終止 listenTo 操做。 if (!obj) return this; // 被監聽對象應該有一個惟一的監聽 ID,即 _listenId,用以標識被監聽者身份。 var id = obj._listenId || (obj._listenId = _.uniqueId('l')); // _listeningTo 是監聽行爲映射表,該表用以保存全部被監聽者的引用。 var listeningTo = this._listeningTo || (this._listeningTo = {}); var listening = listeningTo[id]; // This object is not listening to any other events on `obj` yet. // Setup the necessary references to track the listening callbacks. // 若是被監聽者是首次被當前監聽者監聽,應初始化監聽引用。 if (!listening) { // 監聽者的監聽 ID var thisId = this._listenId || (this._listenId = _.uniqueId('l')); // 監聽引用保存的字段: // obj: 被監聽對象。 // objId: 被監聽對象的監聽 ID。 // id: 監聽者監聽 ID。 // listeningTo: 監聽映射表。 // count: 監聽者對被監聽者監聽的次數 listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0}; } // Bind callbacks on obj, and keep track of them on listening. internalOn(obj, name, callback, this, listening); return this; }; // The reducing API that adds a callback to the `events` object. // 綁定事件 var onApi = function(events, name, callback, options) { // 只有給定事件處理函數才進行事件綁定 if (callback) { // handlers 是事件處理函數組成的數組 var handlers = events[name] || (events[name] = []); // context 事件處理函數上下文(用戶給出),ctx 事件觸發者(默認上下文),listening 監聽引用關係表 var context = options.context, ctx = options.ctx, listening = options.listening; // 監聽計數加一。 if (listening) listening.count++; // 事件處理函數集合增長一個事件處理 handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening }); } return events; }; // Remove one or many callbacks. If `context` is null, removes all // callbacks with that function. If `callback` is null, removes all // callbacks for the event. If `name` is null, removes all bound // callbacks for all events. // 解綁事件 Events.off = function(name, callback, context) { if (!this._events) return this; this._events = eventsApi(offApi, this._events, name, callback, { context: context, // 全部監聽者引用表 listeners: this._listeners }); return this; }; // Tell this object to stop listening to either specific events ... or // to every object it's currently listening to. // 中止監聽 Events.stopListening = function(obj, name, callback) { // 被監聽者引用表 var listeningTo = this._listeningTo; if (!listeningTo) return this; // 須要被中止監聽者 ID 集合(沒有指定被監聽者,則默認全部被監聽者) var ids = obj ? [obj._listenId] : _.keys(listeningTo); // 遍歷被中止監聽者 ID,逐個解除監聽。 for (var i = 0; i < ids.length; i++) { var listening = listeningTo[ids[i]]; // If listening doesn't exist, this object is not currently // listening to obj. Break out early. // 若是 listening 不存在,表示當前沒有監聽行爲。 if (!listening) break; // 被監聽者從自身解除事件行爲。 listening.obj.off(name, callback, this); } // 當沒有監放任何對象時,將 _listeningTo 屬性置爲 void 0。 if (_.isEmpty(listeningTo)) this._listeningTo = void 0; return this; }; // The reducing API that removes a callback from the `events` object. var offApi = function(events, name, callback, options) { // events 不存在,終止 off 操做 if (!events) return; var i = 0, listening; // context 指定上下文,listeners 監聽者 var context = options.context, listeners = options.listeners; // Delete all events listeners and "drop" events. // 沒有給定任何事件名、事件回調或上下文,則移除全部監聽者,以及事件。 if (!name && !callback && !context) { // 生成全部監聽者 id。 var ids = _.keys(listeners); // 遍歷全部監聽者 id,逐一接觸引用關係 for (; i < ids.length; i++) { listening = listeners[ids[i]]; delete listeners[listening.id]; // 移除監聽者引用 delete listening.listeningTo[listening.objId]; // 移除監聽關係 } return; } // 若是沒有指定事件名稱,則移除所有事件 var names = name ? [name] : _.keys(events); for (; i < names.length; i++) { name = names[i]; var handlers = events[name]; // Bail out if there are no events stored. // 若是沒有回調函數,終止本次循環 if (!handlers) break; // Replace events if there are any remaining. Otherwise, clean up. var remaining = []; for (var j = 0; j < handlers.length; j++) { var handler = handlers[j]; if ( callback && callback !== handler.callback && callback !== handler.callback._callback || context && context !== handler.context ) { remaining.push(handler); } else { listening = handler.listening; if (listening && --listening.count === 0) { delete listeners[listening.id]; delete listening.listeningTo[listening.objId]; } } } // Update tail event if the list has any events. Otherwise, clean up. if (remaining.length) { events[name] = remaining; } else { delete events[name]; } } if (_.size(events)) return events; }; // Bind an event to only be triggered a single time. After the first time // the callback is invoked, its listener will be removed. If multiple events // are passed in using the space-separated syntax, the handler will fire // once for each event, not once for a combination of all events. Events.once = function(name, callback, context) { // Map the event into a `{event: once}` object. var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this)); return this.on(events, void 0, context); }; // Inversion-of-control versions of `once`. Events.listenToOnce = function(obj, name, callback) { // Map the event into a `{event: once}` object. var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj)); return this.listenTo(obj, events); }; // Reduces the event callbacks into a map of `{event: onceWrapper}`. // `offer` unbinds the `onceWrapper` after it has been called. var onceMap = function(map, name, callback, offer) { if (callback) { var once = map[name] = _.once(function() { offer(name, once); callback.apply(this, arguments); }); once._callback = callback; } return map; }; // Trigger one or many events, firing all bound callbacks. Callbacks are // passed the same arguments as `trigger` is, apart from the event name // (unless you're listening on `"all"`, which will cause your callback to // receive the true name of the event as the first argument). Events.trigger = function(name) { if (!this._events) return this; var length = Math.max(0, arguments.length - 1); var args = Array(length); for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; eventsApi(triggerApi, this._events, name, void 0, args); return this; }; // Handles triggering the appropriate event callbacks. var triggerApi = function(objEvents, name, cb, args) { if (objEvents) { var events = objEvents[name]; var allEvents = objEvents.all; if (events && allEvents) allEvents = allEvents.slice(); if (events) triggerEvents(events, args); if (allEvents) triggerEvents(allEvents, [name].concat(args)); } return objEvents; }; // A difficult-to-believe, but optimized internal dispatch function for // triggering events. Tries to keep the usual cases speedy (most internal // Backbone events have 3 arguments). var triggerEvents = function(events, args) { var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; switch (args.length) { case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; } }; // Aliases for backwards compatibility. // bind 做爲 on 別名,unbind 做爲 off 別名。(爲向後兼容) Events.bind = Events.on; Events.unbind = Events.off; // Allow the `Backbone` object to serve as a global event bus, for folks who // want global "pubsub" in a convenient place. _.extend(Backbone, Events); // Backbone.Model(模型) // -------------------------- // Backbone **Models** are the basic data object in the framework -- // frequently representing a row in a table in a database on your server. // A discrete chunk of data and a bunch of useful, related methods for // performing computations and transformations on that data. // Create a new model with the specified attributes. A client id (`cid`) // is automatically generated and assigned for you. // // 默認的 Model 構造函數主要作三件事: // 1. 爲實例設置 cid 屬性; // 2. 爲實例設置 attributes; // 3. 調用實例的 initialize 方法完成初始化。 // // 注意: // 若是給定 attributes,它是經過 set 方法添加到 model 的 attributes。 // 而且是早於 initialize 方法調用。 var Model = Backbone.Model = function(attributes, options) { var attrs = attributes || {}; options || (options = {}); // 生成惟一 cid,cid 表示 client id,是指在本地模型變量的標識,而不是模型所表明數據的惟一標識。 this.cid = _.uniqueId(this.cidPrefix); this.attributes = {}; // 若是指定了 collection,則直接綁定到模型上。 if (options.collection) this.collection = options.collection; // 默認初始化設置 attributes 是不通過 parse 方法的(parse 方法只有在 fetch/save 等同步數據時才調用) // 若是指定 options.parse 爲真,則初始化時調用 parse 方法解析 attrs。 if (options.parse) attrs = this.parse(attrs, options) || {}; // 初始化 attributes attrs = _.defaults({}, attrs, _.result(this, 'defaults')); // 調用 set 方法設置初始屬性。 // 不用擔憂 set 會觸發 change 事件,由於此時尚未調用 initialize 方法 // 因此一般說來,此時你還來不及綁定任何事件。 this.set(attrs, options); // 調用 set 方法會致使 this.changed 發生變化, // Jeremy Ashkenas 的意圖是初始化的 Model 不該該含有變化的屬性(由於一切都是初始的) // 因此須要從新將 this.changed 修改成空對象。 // 注意:當設置初始 attributes 時,甚至都尚未調用 initialize。 this.changed = {}; // 調用初始化方法 initialize。 this.initialize.apply(this, arguments); }; // Attach all inheritable methods to the Model prototype. _.extend(Model.prototype, Events, { // 屬性哈希,用以保存模型發生變化的屬性哈希(只有 set 操做會產生舊屬性哈希) changed: null, // set 操做前驗證屬性哈希合法性, // 若是驗證失敗,本屬性保存驗證失敗的結果(model.validate 返回值), // 不然該屬性會被重置爲 null。 validationError: null, // The default name for the JSON `id` attribute is `"id"`. MongoDB and // CouchDB users may want to set this to `"_id"`. idAttribute: 'id', // The prefix is used to create the client id which is used to identify models locally. // You may want to override this if you're experiencing name clashes with model ids. cidPrefix: 'c', // Initialize is an empty function by default. Override it with your own // initialization logic. initialize: function(){}, // 默認實現的 toJSON 方法是複製一份 attributes // 但你能夠覆寫該方法,該方法參數 options 或許用得上。 // 該 options 與 fetch 方法的 options 參數相同,在未指定 HTTP 請求 data 時, // Backbone.sync 會默認使用 `model.toJSON(options)` 來生成 data。 toJSON: function(options) { return _.clone(this.attributes); }, // Proxy `Backbone.sync` by default -- but override this if you need // custom syncing semantics for *this* particular model. // // 模型同步數據,默認委託 Backbone.sync 方法實現本方法。 sync: function() { return Backbone.sync.apply(this, arguments); }, // 獲取屬性值 get: function(attr) { return this.attributes[attr]; }, // 獲取 HTML 轉義後的屬性值。 escape: function(attr) { return _.escape(this.get(attr)); }, // 返回 true,若是模型指定 attribute 不爲 null 或 undefined。 has: function(attr) { return this.get(attr) != null; }, // 委託 _.iteratee 來判斷給定的 attrs 是不是模型 attributes 子集 // 根據傳入 iteratee 參數不一樣,iteratee 具體實現也不一樣。 // 1. attrs 爲 void 0。 // 至關於 _.identity(this.attributes),返回結果爲 true。 // 2. attrs 爲函數。 // 至關於 attrs(this.attributes); // 3. attrs 爲對象。 // 至關於 _.matcher(attrs)(this.attributes),判斷 attrs 是不是 attributes 子集。 // 4. 其餘(主要是指 string) // 至關於 _.property(attrs)(this.attributes); matches: function(attrs) { return !!_.iteratee(attrs, this)(this.attributes); }, // 設置模型屬性哈希,觸發 `change` 事件。 // 本方法是模型對象的核心操做,更新模型數據並將屬性狀態變化通知給外部。 // Backbone.Model 全部更新 attributes 的操做都是經過 set 方法完成, // 例如初始化 initialize(attributes), fetch, save 等。 // 操做成功返回模型對象自身,操做失敗返回 false。 // 注意: // options.slient 爲 true,只是表示本次 set 操做不觸發 `change` 事件。 // 但仍然會更新模型的 `this.changed`, `this._previousAttributes` 屬性, // 所以在調用 `this.hasChanged()`, `this.changedAttributes()` ,`this.previous()`, `this.previousAttributes()` 方式時, // 仍然能夠識別中屬性哈希的變化。 set: function(key, val, options) { // 未指定屬性名稱的操做屬於無效操做。 // 例如:`model.set()` 或 `model.set(null, options)`。 // 所以當須要調用 model.parse 方法時, // 返回值爲 null 或 undefined 將致使模型不設置任何屬性哈希。 if (key == null) return this; // 將 `model.set(key, value, options)` 轉換爲 `model.set({key: value}, options)` 風格。 var attrs; if (typeof key === 'object') { attrs = key; options = val; } else { (attrs = {})[key] = val; } options || (options = {}); // 正式設置屬性哈希前,先驗證輸入參數。 // 若是要求驗證數據,但驗證數據失敗,則停止 set 操做。 // this._validate 方法是經過 this.validate 方法來實現的, // 只有定義了 this.validate 方法,纔會進行驗證,不然默認驗證成功。 // 若是 this.validate 返回值爲真,則表示驗證失敗(返回值就是驗證失敗緣由 this.validationError), // 不然驗證成功,this.validationError 值設置爲 null。 if (!this._validate(attrs, options)) return false; // Extract attributes and options. // // 若是 unset 爲 true,則從 attributes 中移除 key。 // 注意: // 只有 key 存在於 attributes 中,且 value 不等於 attributes[key] 時, // 當 key 被移除時纔會觸發 `change:key` 事件。 var unset = options.unset; var silent = options.silent; // 若是爲 true,不觸發任何 `change` 事件。 var changes = []; // 發生變化屬性名稱列表 // 若是爲 true,表示模型處於 set 操做中。 // 由於 set 操做能夠內嵌在 set 中,this._changing 至關於操做鎖。 // 而局部變量 changing 能夠做爲主動 set 的標識, // 由於只有主動 set 的 changing 此時爲 false,而遞歸 set 中的 changing 都是 true。 var changing = this._changing; this._changing = true; // 若是 set 操做未鎖定,則設置相關屬性 if (!changing) { this._previousAttributes = _.clone(this.attributes); // 保存操做前的屬性哈希副本 this.changed = {}; // (初始)設置變化屬性哈希 } var current = this.attributes; // 當前屬性哈希 var changed = this.changed; // 當前變化屬性哈希 var prev = this._previousAttributes; // 操做前屬性哈希 // 遍歷輸入哈希,更新或刪除哈希值 for (var attr in attrs) { val = attrs[attr]; // 當前屬性值不等於輸入屬性值時,在變化屬性名列表中記錄屬性名稱 if (!_.isEqual(current[attr], val)) changes.push(attr); // 操做前屬性值不等於輸入屬性值時,記錄變化屬性值,不然移除變化屬性名。 // (由於 set 能夠內嵌,this.changed 保存全部內嵌 set 操做結束後的屬性變化狀態) if (!_.isEqual(prev[attr], val)) { changed[attr] = val; } else { delete changed[attr]; } // 若是 options.unset 爲真,則從當前屬性哈希中移除屬性,不然更新當前屬性哈希。 unset ? delete current[attr] : current[attr] = val; } // 更新模型 id,由於 set 可能會更改 idAttribute 指定的主鍵值。 this.id = this.get(this.idAttribute); // Trigger all relevant attribute changes. // 若是 set 不是靜默操做,則須要通知第三方自身屬性的變化。 if (!silent) { // 若是變化屬性名稱列表不爲空,則逐一觸發 `change:key` 事件。 // 而且將輸入 options 設置爲 this._pending。 // `this._pending` 能夠用來緩存輸入 options, // 當在遞歸 set 中有屬性變化時,它能夠不斷被改寫。 // 但只有在主動 set 中臨近操做結束時被讀取。 if (changes.length) this._pending = options; for (var i = 0; i < changes.length; i++) { this.trigger('change:' + changes[i], this, current[changes[i]], options); } } // You might be wondering why there's a `while` loop here. Changes can // be recursively nested within `"change"` events. // // changing 爲真,表示本次 set 爲遞歸操做,主動 set 操做還沒有結束,當即返回。 if (changing) return this; // 本行如下代碼只有在主動 set 操做中才會執行。 // 若是非靜默 set,則須要觸發 `change` 事件。 if (!silent) { // 當 this._pending 爲真時,表示有屬性變化,須要觸發 `change` 事件。 // 而且 this._pending 值就是輸入參數 options。 while (this._pending) { options = this._pending; this._pending = false; this.trigger('change', this, options); } } this._pending = false; // 重置爲 false 表示屬性沒有變化了。 this._changing = false; // 設置爲 false 表示主動 set 操做結束。 return this; }, // 從模型屬性哈希中移除屬性,並觸發 `change` 事件。 // (經過調用 set 方法實現) unset: function(attr, options) { return this.set(attr, void 0, _.extend({}, options, {unset: true})); }, // 從模型屬性哈希中移除全部屬性,觸發 `change` 事件。 // (經過調用 set 方法實現) clear: function(options) { var attrs = {}; for (var key in this.attributes) attrs[key] = void 0; return this.set(attrs, _.extend({}, options, {unset: true})); }, // 判斷模型對象屬性哈希在最後一次 `set` 操做時,是否發生了變化。 // 或者判斷指定的屬性在最後一次 `set` 操做時是否發生了變化。 // 在 set 操做時,使用 options.silent = true 不影響本函數的判斷結果。 hasChanged: function(attr) { if (attr == null) return !_.isEmpty(this.changed); return _.has(this.changed, attr); }, // 本方法有兩個用途: // 1. 當不傳入任何參數時(或 diff 爲否),判斷最後一次 set 操做,屬性哈希是否發生變化。 // 若是發生變化,返回變化屬性哈希,不然返回 false。 // // 2. 傳一個 Object 對象做爲 diff 參數,將其與模型當前屬性哈希進行對比, // 篩選出於不一樣於當前屬性哈希的屬性,若是有篩選結果,則返回篩選結果,不然返回 false。 // 使用 `model.changedAttributes(someObject)` 能夠(預先)判斷出 set 哪些值會致使模型的屬性哈希發生變化。 changedAttributes: function(diff) { if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; var old = this._changing ? this._previousAttributes : this.attributes; var changed = {}; for (var attr in diff) { var val = diff[attr]; if (_.isEqual(old[attr], val)) continue; changed[attr] = val; } return _.size(changed) ? changed : false; }, // 返回最後一次 set 以前的指定屬性值(不管該屬性是否發生過變化)。 // 不傳入參數,或者模型沒有進行過 set 操做,返回 null; previous: function(attr) { if (attr == null || !this._previousAttributes) return null; return this._previousAttributes[attr]; }, // 返回最後一次 set 前的屬性哈希。 // 若是沒有 set 過,則返回 null。 previousAttributes: function() { return _.clone(this._previousAttributes); }, // Fetch the model from the server, merging the response with the model's // local attributes. Any changed attributes will trigger a "change" event. // fetch 方法主要用於從遠端讀取數據同步到本地 attributes,若是屬性值發生變化,觸發 `change` 事件。 // Backbone 本意是爲 REST API 而設計,但也能夠兼容非 REST API。 // 使用非 REST API 時,應該改寫 parse 方法,再調用 fetch 方法。 fetch: function(options) { // 遠端的響應結果默認須要經過 parse 方法解析才能 set, // 能夠在 options 中指定 parse 爲 false 來解除這一邏輯。 options = _.extend({parse: true}, options); var model = this; // 封裝 success 操做 // 不管 options 中是否指定 success 回調,xhr 請求成功後都會有一次 success 回調。 // 若是有 options.success 回調函數,回調函數會在封裝的 success 回調用執行。 var success = options.success; options.success = function(resp) { // 若是要求 parse 爲真,則遠程返回值必須通過 parse 方法解析,不然遠程返回值就是響應數據。 var serverAttrs = options.parse ? model.parse(resp, options) : resp; // model.set 方法只有在 validate 失敗時纔會返回 false。 // 若是驗證失敗,則不會進行具備實際意義的 set 操做。 // 而且觸發 invalid 事件。 if (!model.set(serverAttrs, options)) return false; // 若是 set 操做成功,則繼續調用本來計劃的 success 回調函數。 // 注意: // 此處的 success 回調與原生 jQuery ajax success 回調稍微不一樣的是, // 它的上下文由 options.context 指定。 if (success) success.call(options.context, model, resp, options); // 執行完 success 回調後觸發 sync 事件。 model.trigger('sync', model, resp, options); }; // 封裝 options.error 回調,確保 xhr 失敗時,觸發 model 的 error 事件。 wrapError(this, options); // read 遠程數據,默認使用 this.sync 方法實現, // this.sync 默認使用 Backbone.sync 方法實現, // Backbone.sync 默認使用 Backbone.$ 方法實現, // Backbone.$ 默認使用 jQuery.ajax 方法實現。 return this.sync('read', this, options); }, // Set a hash of model attributes, and sync the model to the server. // If the server returns an attributes hash that differs, the model's // state will be `set` again. // // 設置 model 的 attributes,而且將 attributes 同步到遠端。 // 若是遠端返回的響應值(經過 parse 方法解析後)不一樣於 attributes, // 則再次執行 set 操做。 save: function(key, val, options) { // Handle both `"key", value` and `{key: value}` -style arguments. // 處理不一樣傳參方式。 var attrs; if (key == null || typeof key === 'object') { attrs = key; options = val; } else { (attrs = {})[key] = val; } // 默認要求進行 validate 和 parse 操做。 options = _.extend({validate: true, parse: true}, options); // 是否等待服務器響應再進行 set 操做的標識。(默認不等待) // 等待服務器響應與否的區別是: // 一個先 set,後同步數據。 // 一個是先同步數據,而後 set。 var wait = options.wait; // If we're not waiting and attributes exist, save acts as // `set(attr).save(null, opts)` with validation. Otherwise, check if // the model will be valid when the attributes, if any, are set. // 若是 attrs 爲真,且無需等待服務器響應,則當即使用 attrs 進行 set 操做。 // 注意: // 若是不等待服務器響應,set 操做一旦成功會當即觸發 `change` 事件, // 但隨後的服務器響應值會被從新 set 一次,有可能會 set 失敗。 if (attrs && !wait) { // 若是 set 操做失敗(即 validate 失敗),當即返回 false(結束 save 操做)。 // 注意:什麼要求 attrs 也爲真才進行 set 操做? // 若是不限制 attrs 爲真,那麼 set 操做會默認成功,則將致使 save 操做不會終止。 // 那麼 save 會將未作任何修改的 attributes 再次同步到遠端,這樣不符合 save 操做的意圖。 if (!this.set(attrs, options)) return false; } else { // 驗證 attrs,驗證失敗則當即終止 save 操做。 if (!this._validate(attrs, options)) return false; } // After a successful server-side save, the client is (optionally) // updated with the server-side state. // // 如下邏輯與 fetch 邏輯類似,開始進行數據同步相關操做。 var model = this; var success = options.success; var attributes = this.attributes; // 封裝 success 回調 options.success = function(resp) { // Ensure attributes are restored during synchronous saves. model.attributes = attributes; // 解析遠端響應值 var serverAttrs = options.parse ? model.parse(resp, options) : resp; // 若是當前 save 操做是須要等待服務器響應的,則合併 attrs 和 serverAttrs 屬性, // 而後再進行 set 操做。 if (wait) serverAttrs = _.extend({}, attrs, serverAttrs); // 不管是否要求 save 操做等待服務器響應,若是響應值存在(通過可能的 parse 操做後,解析結果不爲 null 或 undefined), // 則進行 set 操做,set 操做失敗(驗證失敗)會當即終止 save 操做。 if (serverAttrs && !model.set(serverAttrs, options)) return false; // set 成功後,執行可能計劃的 success,而後觸發 sync 事件。 if (success) success.call(options.context, model, resp, options); model.trigger('sync', model, resp, options); }; // 封裝 error 回調 wrapError(this, options); // Set temporary attributes if `{wait: true}` to properly find new ids. // 設置臨時的 attributes,由於在 Backbone.sync 操做中可能須要將 attributes 同步到遠端。 // 注意:此處是直接修改 attributes,而不是經過 set 操做進行修改,所以不會觸發任何事件。 if (attrs && wait) this.attributes = _.extend({}, attributes, attrs); // 根據模型狀態選擇 REST API 的提交方式 var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); // 若是是 patch 操做,則將 attrs 保存在 options.attrs 中 // (此處保存 attrs 的意圖不是很明確,難道只是爲了記錄下 patch 的數據,以便知足開發者的個性化操做?) if (method === 'patch' && !options.attrs) options.attrs = attrs; var xhr = this.sync(method, this, options); // Restore attributes. // 馬上恢復模型應該擁有的 attributes。 this.attributes = attributes; return xhr; }, // Destroy this model on the server if it was already persisted. // Optimistically removes the model from its collection, if it has one. // If `wait: true` is passed, waits for the server to respond before removal. // 銷燬模型(並同步從遠端銷燬) // 若是 options.wait 爲真,則等待遠端同步成功後,再銷燬模型。 // destroy 操做主要實現: // 1. stopListening 全部事件(不包括自身的 on 事件) // 2. 觸發 destroy 事件(通知 collection 將本身從 collection 中移除) // 3. (可選)同步從遠端刪除數據。(根據 model.isNew() 判斷是否要觸發 sync 事件) // // 注意: // Backbone.sync 期待的是 RESTFUL API,若是使用 emulatedHTTP, // destroy 操做 success 會在 XHR 請求成功後當即執行, // 即 XHR 請求成功,即視爲遠端刪除數據成功。 // 所以在不重寫 destroy 的方法前提下,要求遠端接口響應必需以 HTTP Status Code(200 或 404)做爲操做成功失敗的標識, // 而不能在響應的 data 中約定操做成功失敗代號。 destroy: function(options) { options = options ? _.clone(options) : {}; var model = this; var success = options.success; var wait = options.wait; // 銷燬模型(中止監聽事件,觸發 destroy 事件) var destroy = function() { model.stopListening(); model.trigger('destroy', model, model.collection, options); }; // 封裝 success 回調 // 該回調會在請求成功後當即執行,請求成功即被視爲操做成功。 options.success = function(resp) { if (wait) destroy(); if (success) success.call(options.context, model, resp, options); // 若是 model.isNew() 爲假,纔有可能會觸發 sync 事件。 if (!model.isNew()) model.trigger('sync', model, resp, options); }; var xhr = false; // 若是模型數據不存在於遠端(按照 Backbone 設計理論), // 則無需與遠端進行數據同步操做,直接執行 success 回調。(理論上也不觸發 sync 事件) if (this.isNew()) { _.defer(options.success); } else { // 封裝異常回調 wrapError(this, options); // 與遠端同步 xhr = this.sync('delete', this, options); } // 若是不等待,則當即銷燬模型。 if (!wait) destroy(); return xhr; }, // Default URL for the model's representation on the server -- if you're // using Backbone's restful methods, override this to change the endpoint // that will be called. // 生成 model 進行 sync 時的 URL(適用於 RESTFUL API) // 默認的 url 方法主要適用於 RESTFUL API,自動生成 URL。 // 對於非 RESTFUL API,最好重寫該方法。 url: function() { var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); if (this.isNew()) return base; var id = this.get(this.idAttribute); // 自動補齊 base 末尾的 `/` 符號,而後追加 id return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id); }, // parse 方法存在的意義在於解析遠程同步數據時,遠端返回的響應對象, // 該對象默認是 REST API 返回的數據對象,所以能夠直接被 set 方法使用。 // 但對於非 REST API 接口響應對象,則須要調用 parse 將其響應結果解析後再返回給 set 使用。 // 因此說,若是直接使用 set 方法設置屬性,是無需通過 parse 方法的,只有自動同步遠程數據時才須要覆寫該方法。 // 例如 fetch 方法中調用了 parse 方法解析遠端的響應值,fetch 方法中使用 parse 解析結果去作 set 操做, // 所以 parse 返回 null 或 undefined 時,set 操做會當即終止。 parse: function(resp, options) { return resp; }, // Create a new model with identical attributes to this one. clone: function() { return new this.constructor(this.attributes); }, // A model is new if it has never been saved to the server, and lacks an id. // 判斷一個 model 是否從未保存到遠端。 // 判斷依據是查看該 model 的 attributes 是否擁有 this.idAttribute 映射字段。 // Backbone.Model 的設計意圖是 Model 是遠端一條數據的抽象對象(例如數據庫中某張表裏某一行數據), // 每一個 model 都應該擁有一個主鍵(對應數據庫裏數據行的主鍵值),擁有主鍵則表示遠端已存在該條數據, // 不然視該 model 爲未保存的數據模型。 isNew: function() { return !this.has(this.idAttribute); }, // Check if the model is currently in a valid state. // 檢查 model 當前的 attributes 是否處於合法狀態(可以經過驗證) isValid: function(options) { return this._validate({}, _.defaults({validate: true}, options)); }, // Run validation against the next complete set of model attributes, // returning `true` if all is well. Otherwise, fire an `"invalid"` event. // 注意: // validate 是對模擬 set 成功後的 attributes 進行驗證,而不只僅是對 attrs 進行驗證。 // 也就是說 this.validate(attrs, options) 中的 attrs 是指模擬 set 成功後的 attributes。 _validate: function(attrs, options) { if (!options.validate || !this.validate) return true; attrs = _.extend({}, this.attributes, attrs); var error = this.validationError = this.validate(attrs, options) || null; if (!error) return true; this.trigger('invalid', this, error, _.extend(options, {validationError: error})); return false; } }); // Underscore methods that we want to implement on the Model, mapped to the // number of arguments they take. var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0, omit: 0, chain: 1, isEmpty: 1 }; // Mix in each Underscore method as a proxy to `Model#attributes`. addUnderscoreMethods(Model, modelMethods, 'attributes'); // Backbone.Collection // ------------------- // If models tend to represent a single row of data, a Backbone Collection is // more analogous to a table full of data ... or a small slice or page of that // table, or a collection of rows that belong together for a particular reason // -- all of the messages in this particular folder, all of the documents // belonging to this particular author, and so on. Collections maintain // indexes of their models, both in order, and for lookup by `id`. // Create a new **Collection**, perhaps to contain a specific type of `model`. // If a `comparator` is specified, the Collection will maintain // its models in sort order, as they're added and removed. // // Collection 構造函數,能夠指定 collection 的 model 類型。 // 若是給定 `comparator`,當天新增或移除 model 時,collection 會自動維護 models 的排序。 var Collection = Backbone.Collection = function(models, options) { options || (options = {}); // 若是 options 中包含 model 字段,則直接綁定到 collection。 if (options.model) this.model = options.model; // 若是 options 中包含 comparator 且不爲 undefined,則直接綁定到 collection。 if (options.comparator !== void 0) this.comparator = options.comparator; // 重置 collection 的 length, models, _byId 三個屬性。 this._reset(); // 初始化 collection this.initialize.apply(this, arguments); // 若是指定了 models, 則靜默設置初始 models // todo: // 與 model 初始化不一樣,collection 使用 reset 而不是 set 做爲構造初始數據的手段, // 且 reset 操做晚於 initialize 操做。做者意圖不是很明確。 // 如此操做的話,則意味着你不該該在 initialize 中對 collection 進行成員增減操做, // 不然可能會在構造實例時,被構造參數中的 models 覆寫了 collection 成員。 if (models) this.reset(models, _.extend({silent: true}, options)); }; // collection#set 操做的默認選項。 var setOptions = {add: true, remove: true, merge: true}; var addOptions = {add: true, remove: false}; // 將數組 insert 成員,依次插入到數組 array 的 at 位置。 // 例如: // var a = [1,2,3], b = [4,5,6]; // splice(a, b, 1); // 數組 a 變成 [1, 4, 5, 6, 2, 3] var splice = function(array, insert, at) { // 確保 at 是符合 array 長度的合法位置(不小於 0,不大於 array 長度)。 at = Math.min(Math.max(at, 0), array.length); // 生成切片後半部分等長 Array。 var tail = Array(array.length - at); // 計算待插入 Array 長度 var length = insert.length; // 將 array 後半部分紅員複製到容器 tail。 for (var i = 0; i < tail.length; i++) tail[i] = array[i + at]; // 將 insert 成員依次插入到 array 的後半部分。 for (i = 0; i < length; i++) array[i + at] = insert[i]; // 將 tail 中成員依次繼續插入到 array 尾部。 for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i]; }; // Define the Collection's inheritable methods. _.extend(Collection.prototype, Events, { // 默認 model 爲 Backbone.Model。 // 大部分情景中你須要重寫該屬性。 model: Model, // Initialize is an empty function by default. Override it with your own // initialization logic. initialize: function(){}, // 返回一個數組,成員是 collection 中每一個 model 的 JSON 值。 toJSON: function(options) { return this.map(function(model) { return model.toJSON(options); }); }, // Proxy `Backbone.sync` by default. sync: function() { return Backbone.sync.apply(this, arguments); }, // Add a model, or list of models to the set. `models` may be Backbone // Models or raw JavaScript objects to be converted to Models, or any // combination of the two. // // 使用 set 操做往 collection 添加一個或多個成員。 // models 能夠是 Backbone.Model 及其子類實例,或者是純 Object,或者兩者混合組成的數組。 // 關於 options: // 默認 merge 爲 false,但容許指定爲 true。 // 強制 add 爲 true,remove 爲 false,不容許修改。 // // add 操做的默認行爲是在 collection 末尾追加成員,若是成員已經存在,則不追加。 add: function(models, options) { return this.set(models, _.extend({merge: false}, options, addOptions)); }, // Remove a model, or a list of models from the set. // // 從 collection 中移除一個或一組成員。 remove: function(models, options) { options = _.extend({}, options); var singular = !_.isArray(models); models = singular ? [models] : _.clone(models); // 移除成員 var removed = this._removeModels(models, options); // 若是 remove 操做非靜默,而且的確移除了成員, // 觸發 update 事件。 if (!options.silent && removed) this.trigger('update', this, options); // 返回被移除的成員(們)。 return singular ? removed[0] : removed; }, // Update a collection by `set`-ing a new list of models, adding new ones, // removing models that are no longer present, and merging models that // already exist in the collection, as necessary. Similar to **Model#set**, // the core operation for updating the data contained by the collection. // // 該方法是 collection 操做 models 的核心方法,重要性等同於 Model#set 方法。 // 該方法用以設置一組新的成員,添加新成員,刪除再也不具備成員資格的成員,合併已存在的成員。 // // options: // add: 若是 model 存在於 models 中但不存在於 collection 中,是否要往 collection 中添加該 model。(默認爲真) // remove: 若是 model 存在於 collection 中但不存在於 models 中,是否要從 collection 中刪除該 model。(默認爲真) // merge: 若是 model 存在於 models 中同時也存在於 collection 中,是否要將兩者進行合併。(默認爲真) // silent: 是否要觸發事件(默認爲真) // sort: 是否要自動排序(默認爲真) // parse: set 操做前是否要通過 parse 方法解析,包括經過純 Object 生成 Model 實例時,是否要調用 Model.parse 解析(默認爲假) // // collection#set 操做的本質是,將目標 models 數組中的數據,合併到內部 models 數組中, // 兩個數組的數據合併,涉及到求數據交集、求數據並集、是否合併數據的問題。set 操做就是實現了這三個問題的解決方法。 // 在全部對 models 操做過程當中,collection 始終保持對實例 models 的引用一致性(即歷來沒有更換過 models 數組的指針) // // set 操做中,會對根據每一個新增的成員和移除的成員依次觸發 add 和 remove 事件。 // 因此雖然 set 操做能夠經過 options 中 add 和 remove 的值,來實現置換整個 collection.models 內部全部成員, // 但你的意圖是徹底置換而不想逐一觸發 add 或 remove 事件,那麼最好使用 collection#reset 操做,該操做只會觸發一個 `reset` 事件。 // // 若是 models 爲 null 或 void 0,會致使 set 操做終止。 // 但若是 parse 方法返回值爲 null 或 void 0,或者 parse 方法返回的數組中包含 null 或 void 0,都會被視爲一個合法成員, // collection 會首先尋找該成員是否存在,若是不存在則視爲新成員,使用 this.model 來構造新的實例,因此若是該 `成員` 爲 null 或 void 0, // 新實例也會被構造出來並可能被添加到 collection(除非 model 實例在構造時未能經過合法性驗證)。 // // 注意: // options.parse 對 collection#set 方法有效,而對 model#set 方法無效。 set: function(models, options) { // 若是 models 爲 null 或 undefined,終止 set 操做。 if (models == null) return; // 準備 options,默認 add: true, remove: true, merge: true。 options = _.defaults({}, options, setOptions); // 若是 options.parse 爲真,且 models 非 Backbone.Model 實例, // 則調用 this.parse 方法對 models 進行解析。 if (options.parse && !this._isModel(models)) models = this.parse(models, options); // 若是 models 不是數組,則將其轉換爲數組。 // 注意: // 此處操做實際上是讀取了 models 副本,而非原始 models, // 以免後面對 models 的操做會影響到輸入的 models。 var singular = !_.isArray(models); models = singular ? [models] : models.slice(); var at = options.at; // 插入新成員的位置 if (at != null) at = +at; // 將 at 強轉爲數字類型 if (at < 0) at += this.length + 1; // 若是 at 是負數,則表示位置是倒數的,將其轉換爲實際位置。 var set = []; // 置換的成員容器 var toAdd = []; // 新添加的成員容器 var toRemove = []; // 待刪除的成員容器 var modelMap = {}; // 置換成員映射表 var add = options.add; // 新增標識 var merge = options.merge; // 合併標識 var remove = options.remove; // 移除標識 var sort = false; // 是否須要排序 // 是否具有排序條件(必需定義了 comparator,不能指定插入位置,沒有顯式聲明不排序) var sortable = this.comparator && (at == null) && options.sort !== false; var sortAttr = _.isString(this.comparator) ? this.comparator : null; // 若是 comparator 是字符串,則表示使用 model 某個屬性做爲排序因子 // Turn bare objects into model references, and prevent invalid models // from being added. var model; // 遍歷 models,處理那些須要被添加到 collection 的 model for (var i = 0; i < models.length; i++) { model = models[i]; // If a duplicate is found, prevent it from being added and // optionally merge it into the existing model. // 查找待添加 model 是否已存在於 collection 中。 var existing = this.get(model); if (existing) { // 若是 collection 已保存有目標 model,而且待添加 model 不等於已存在 model。 // 即待添加的是另外一個模型實例或者 Object,且 merge 爲真,則合併新的 model。 if (merge && model !== existing) { // 若是待添加 model 爲 Backbone.Model 實例,獲取它的 attributes 做爲 attrs。 var attrs = this._isModel(model) ? model.attributes : model; // 若是 parse 爲真,則須要調用 existing 的 parse 方法來解析 attrs, // 以後才能對 existing 進行 set 操做。 if (options.parse) attrs = existing.parse(attrs, options); existing.set(attrs, options); // 若是具有排序條件,而且沒有排序,則從新設定 sort 以標識是否須要排序。 // 若是 existing 中做爲排序的因子屬性發生了變化,則須要將 sort 設置爲真,表示要排序。 if (sortable && !sort) sort = existing.hasChanged(sortAttr); } if (!modelMap[existing.cid]) { modelMap[existing.cid] = true; set.push(existing); } // 將 models 中待添加的 model 替換爲 existing。 models[i] = existing; // If this is a new, valid model, push it to the `toAdd` list. } else if (add) { // 若是 options.add 爲真,則準備一個待處理的 model。 model = models[i] = this._prepareModel(model, options); // model 只能是一個 Model 實例或者 false, // 若是是 false,則表示該 model 不是一個合法的 model,直接忽略。 if (model) { toAdd.push(model); // 添加 model 與 collection 之間的引用關係 this._addReference(model, options); modelMap[model.cid] = true; set.push(model); } } } // Remove stale models. // 若是 options.remove 爲真,則須要移除 collection.models 中多餘的 model。 if (remove) { // 遍歷 this.models,篩選中待移除的 model,保存在 toRemove 中 for (i = 0; i < this.length; i++) { model = this.models[i]; // 若是置換成員映射中不包含該 model,則表示它須要被移除。 if (!modelMap[model.cid]) toRemove.push(model); } // 移除多餘的 model。 if (toRemove.length) this._removeModels(toRemove, options); } // See if sorting is needed, update `length` and splice in new models. // 標識成員的順序是否發生變化,該標識主要充當是否觸發 sort 事件的條件因子。 var orderChanged = false; // 是否直接置換 collection.models var replace = !sortable && add && remove; if (set.length && replace) { // 若是置換的成員數量與現有成員數量不符,或者任意置換成員與現有成員位置不符,則表示須要從新排序。 orderChanged = this.length != set.length || _.some(this.models, function(model, index) { return model !== set[index]; }); // 清空現有全部成員 this.models.length = 0; // 插入置換成員 splice(this.models, set, 0); // 實時維護 length 屬性 this.length = this.models.length; } else if (toAdd.length) { // 若是具有排序條件,sort 設置爲 true if (sortable) sort = true; // 將新的成員插入到 this.models,若是未指定插入位置,則從最末尾插入。 splice(this.models, toAdd, at == null ? this.length : at); // 實時維護 collection.length 屬性 this.length = this.models.length; } // Silently sort the collection if appropriate. // 若是須要排序,則靜默排序(阻止排序過程當中觸發 sort 事件) if (sort) this.sort({silent: true}); // Unless silenced, it's time to fire all appropriate add/sort events. // 如今來處理一下事件的事情, // 若是如非靜默操做,須要依次觸發可能存在的 add, sort, update 事件。 if (!options.silent) { for (i = 0; i < toAdd.length; i++) { if (at != null) options.index = at + i; model = toAdd[i]; model.trigger('add', model, this, options); } if (sort || orderChanged) this.trigger('sort', this, options); if (toAdd.length || toRemove.length) this.trigger('update', this, options); } // Return the added (or merged) model (or models). // 返回單個 model 或 models return singular ? models[0] : models; }, // When you have more items than you want to add or remove individually, // you can reset the entire set with a new list of models, without firing // any granular `add` or `remove` events. Fires `reset` when finished. // Useful for bulk operations and optimizations. // 使用 reset 操做替代 set 操做來重置整個 collection.models,避免觸發 add 或 remove 事件, // 只有一個 `reset` 事件。 // 注意: // reset 操做會簡單地從新生成一個空數組,並將該數組指針賦值給 collection.models, // 而以前的 collection.models 會保留在 options.previousModels 做爲參數傳遞給 reset 事件。 // // reset 與 set 不一樣之處在於,set 操做維持 collection.models 指針不變,而 reset 會更換 collection.models 指針。 reset: function(models, options) { options = options ? _.clone(options) : {}; // 遍歷現有成員,逐一銷燬成員與集合之間的引用關係 for (var i = 0; i < this.models.length; i++) { this._removeReference(this.models[i], options); } // 保留以前的 models 引用 options.previousModels = this.models; // 重置內部狀態(包括更換 this.models) this._reset(); // 調用 add 操做添加成員(add 操做內部是調用 set 操做) models = this.add(models, _.extend({silent: true}, options)); // 觸發 reset 事件 if (!options.silent) this.trigger('reset', this, options); return models; }, // Add a model to the end of the collection. // 其實 push 等同於 add,你也能夠在 options 中指定 at 做爲插入位置。 // 並且 model 能夠是單個也能夠是多個。 push: function(model, options) { return this.add(model, _.extend({at: this.length}, options)); }, // Remove a model from the end of the collection. // 移除最後一個成員。 pop: function(options) { var model = this.at(this.length - 1); return this.remove(model, options); }, // Add a model to the beginning of the collection. // 在內部 models 數組頭部追加成員。 // 等同於 add 操做,能夠經過 options.at 參數修改 unshift 行爲 unshift: function(model, options) { return this.add(model, _.extend({at: 0}, options)); }, // Remove a model from the beginning of the collection. // 移除第一個成員 shift: function(options) { var model = this.at(0); return this.remove(model, options); }, // Slice out a sub-array of models from the collection. // 對 this.models 進行切片操做。 slice: function() { return slice.apply(this.models, arguments); }, // Get a model from the set by id. // 查找成員,obj 能夠是一個 id 值, // 或者是一個包含 collection.model.prototype.idAttribute 屬性的對象, // 或者是一個 model 實例。 // collection 首先嚐試使用 id 查找,而後使用 cid 查找。 get: function(obj) { if (obj == null) return void 0; var id = this.modelId(this._isModel(obj) ? obj.attributes : obj); return this._byId[obj] || this._byId[id] || this._byId[obj.cid]; }, // Get the model at the given index. // 獲取指定位置的成員,若是 at 爲負數,表示倒數位置。 at: function(index) { if (index < 0) index += this.length; return this.models[index]; }, // Return models with matching attributes. Useful for simple cases of // `filter`. // 查找成員 where: function(attrs, first) { return this[first ? 'find' : 'filter'](attrs); }, // Return the first model with matching attributes. Useful for simple cases // of `find`. // 查找成員 findWhere: function(attrs) { return this.where(attrs, true); }, // Force the collection to re-sort itself. You don't need to call this under // normal circumstances, as the set will maintain sort order as each item // is added. // // 強制對 collection 成員進行排序,但若是沒有聲明 comparator,則拋出異常。 sort: function(options) { var comparator = this.comparator; if (!comparator) throw new Error('Cannot sort a set without a comparator'); options || (options = {}); // length 變量記錄 comparator 長度,主要意圖是記錄 comparator 做爲函數時,期待參數的個數。 var length = comparator.length; if (_.isFunction(comparator)) comparator = _.bind(comparator, this); // Run sort based on type of `comparator`. // 若是 comparator 是一個接受單個參數的函數,或者字符串, // 則使用 sortBy 進行(升序)排序,不然對 models 進行原生數組排序。 if (length === 1 || _.isString(comparator)) { this.models = this.sortBy(comparator); } else { this.models.sort(comparator); } // 若是 sort 爲非靜默操做,則觸發 sort 事件 if (!options.silent) this.trigger('sort', this, options); return this; }, // Pluck an attribute from each model in the collection. // 獲取每一個成員指定的 attribute。 // 注意:這裏使用 get 方法獲取屬性,而不是直接使用 _.pluck(this.toJSON(), attr); // 這樣作避免了直接讀取 model.attributes,若是 model 的 get 方法被改寫了,也能夠正確返回相應的值。 pluck: function(attr) { return _.invoke(this.models, 'get', attr); }, // Fetch the default set of models for this collection, resetting the // collection when they arrive. If `reset: true` is passed, the response // data will be passed through the `reset` method instead of `set`. // // 與 Model#fetch 方法相似,若是 options.reset 爲真,則使用 collection.reset 處理遠端響應, // 不然使用 collection.set 處理遠端響應。 fetch: function(options) { options = _.extend({parse: true}, options); var success = options.success; var collection = this; options.success = function(resp) { var method = options.reset ? 'reset' : 'set'; collection[method](resp, options); if (success) success.call(options.context, collection, resp, options); collection.trigger('sync', collection, resp, options); }; wrapError(this, options); return this.sync('read', this, options); }, // Create a new instance of a model in this collection. Add the model to the // collection immediately, unless `wait: true` is passed, in which case we // wait for the server to agree. // 經過 Model#save 方法實現的建立 model。 create: function(model, options) { options = options ? _.clone(options) : {}; var wait = options.wait; // 準備 model,若是準備 model 失敗,則直接終止 create 操做,並返回 false。 model = this._prepareModel(model, options); if (!model) return false; // 若是不等待服務器響應,則直接添加 model 到 collection.models。 // 這意味着,不管 model 是否 validate 與否,它都會被添加到 collection 中。 // 由於 model 實例化過程當中,不管 validate 成功失敗,都不能阻止 model 構造完成。 // 而等待服務器響應,在 model.save 過程當中,能夠對 attributes 進行合法性驗證, // 從而阻止 options.success 被調用,也就阻止了非法的 model 被添加到 collection 中。 if (!wait) this.add(model, options); // 不然等到服務器響應成功後再將 model 添加到 collection var collection = this; var success = options.success; options.success = function(model, resp, callbackOpts) { if (wait) collection.add(model, callbackOpts); if (success) success.call(callbackOpts.context, model, resp, callbackOpts); }; // 經過 model.save 方法實現 create,即 colleciton 自己是不負責真正的 model 數據同步。 model.save(null, options); return model; }, // **parse** converts a response into a list of models to be added to the // collection. The default implementation is just to pass it through. // // 本方法將遠端響應轉換爲一個列表(待添加成員列表)或者是一個成員對象。 // parse 返回任何對象(包括 null, undefined),若是不是數組,都會被轉換爲數組, // 而後被 collection 用做查找已存在成員的因子,或者做爲 this.model 構造函數的 attributes 參數。 parse: function(resp, options) { return resp; }, // Create a new collection with an identical list of models as this one. clone: function() { return new this.constructor(this.models, { model: this.model, comparator: this.comparator }); }, // Define how to uniquely identify models in the collection. // // 該方法主要用於讓 collection 給每一個成員生成一個惟一標識。 // collection 內部須要斷定成員身份的操做都須要調用該方法。 modelId: function (attrs) { return attrs[this.model.prototype.idAttribute || 'id']; }, // 私有方法,重置 collection 內部狀態(主要是 collection 的 length, models, _byId)。 // 只有在 collection 進行初始化或 reset 操做時才調用該方法。 _reset: function() { // Collection 是實時維護 length 屬性, // 而不是經過 this.models.length 求值來獲取成員長度。 this.length = 0; this.models = []; this._byId = {}; }, // Prepare a hash of attributes (or other model) to be added to this // collection. // // _prepareModel: function(attrs, options) { // 若是 attrs 是一個 Backbone.Model 實例,且該模型未屬於其餘 collection,則爲其添加 collection 屬性。 // 這意味着一個 model 不能同時關聯到兩個 collection。 if (this._isModel(attrs)) { if (!attrs.collection) attrs.collection = this; return attrs; } // 若是 attrs 不是 Backbone.Model 實例, // 則使用 this.model 做爲構造函數,構造一個 Backbone.Model 實例。 options = options ? _.clone(options) : {}; options.collection = this; var model = new this.model(attrs, options); // 若是構造 model 實例過程當中,沒有發生數據驗證失敗, // 則表示新構造的 model 是一個合法的 model,直接返回該 model。 // 不然在 collection 觸發 invalid 事件,並返回 false。 if (!model.validationError) return model; this.trigger('invalid', this, model.validationError, options); return false; }, // Internal method called by both remove and set. // // 私有方法,在 remove 和 set 操做中調用,用以移除成員。 _removeModels: function(models, options) { // 回收被移除成員的容器 var removed = []; // 遍歷移除條件對象 models for (var i = 0; i < models.length; i++) { // 查找待移除成員 var model = this.get(models[i]); // 如未找到則進行下一輪循環 if (!model) continue; // 查找待移除成員的位置,並從 this.models 中將其移除 var index = this.indexOf(model); this.models.splice(index, 1); // 將 collection 的 length 屬性減一 this.length--; // 若是 remove 操做非靜默,則觸發 remove 事件。 // 在 options 中記錄被移除成員的位置。 if (!options.silent) { options.index = index; model.trigger('remove', model, this, options); } // 在回收容器中保存被移除成員 removed.push(model); // 銷燬被移除成員與 collection 之間的引用關係 this._removeReference(model, options); } // 返回 false 表示沒有任何成員被移除,不然返回全部被移除成員的集合 return removed.length ? removed : false; }, // Method for checking whether an object should be considered a model for // the purposes of adding to the collection. // // 私有方法,檢查 model 是不是 Backbone.Model 實例 _isModel: function (model) { return model instanceof Model; }, // Internal method to create a model's ties to a collection. // 私有方法,添加成員與集合之間的引用關係。 _addReference: function(model, options) { // 在 this._byId 中添加成員映射關係 // 首先使用成員的 cid 添加映射, // 而後經過 modelId 對成員求值,添加映射關係。 // 也就是說,一般 collection 會保存對成員的兩個引用關係, // 一個是經過 cid,另外一個是經過 idAttribute。 this._byId[model.cid] = model; var id = this.modelId(model.attributes); if (id != null) this._byId[id] = model; // 爲 model 添加 all 事件回調。 // 這裏是經過成員的 on 方法添加回調,而不是 listenTo 成員, // 所以若是成員執行 model.off('all'),那麼成員的任何事件都不會再轉發到 collection。 // 很難說此處使用 listenTo 或 on 的優劣,但使用 on,則將事件主動權交到了成員手中。 model.on('all', this._onModelEvent, this); }, // Internal method to sever a model's ties to a collection. // // 私有方法,用以銷燬成員與集合之間的引用關係。 _removeReference: function(model, options) { // 依次刪除使用 cid 與 idAttribute 對成員進行引用的關係 delete this._byId[model.cid]; var id = this.modelId(model.attributes); if (id != null) delete this._byId[id]; // 銷燬 model 的 collection 屬性 if (this === model.collection) delete model.collection; // 從 model 的 all 事件回調隊列中,移除與本 collection 相關的回調函數。 model.off('all', this._onModelEvent, this); }, // Internal method called every time a model in the set fires an event. // Sets need to update their indexes when models change ids. All other // events simply proxy through. "add" and "remove" events that originate // in other collections are ignored. // // 響應成員的 all 事件 _onModelEvent: function(event, model, collection, options) { // add 和 remove 是來自 collection 自身,所以再也不轉發該兩個事件。 if ((event === 'add' || event === 'remove') && collection !== this) return; // 當成員發生 destroy 事件時,從 collection 移除該成員。 if (event === 'destroy') this.remove(model, options); if (event === 'change') { // 當成員發生 change 事件時,意味着成員的 id 屬性可能發生變化, // 因此須要在 collection 中從新檢視成員的 idAttribute 引用關係。 var prevId = this.modelId(model.previousAttributes()); var id = this.modelId(model.attributes); if (prevId !== id) { if (prevId != null) delete this._byId[prevId]; if (id != null) this._byId[id] = model; } } // 轉發成員事件 this.trigger.apply(this, arguments); } }); // Underscore methods that we want to implement on the Collection. // 90% of the core usefulness of Backbone Collections is actually implemented // right here: var collectionMethods = { forEach: 3, each: 3, map: 3, collect: 3, reduce: 4, foldl: 4, inject: 4, reduceRight: 4, foldr: 4, find: 3, detect: 3, filter: 3, select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3, contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3, head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3, without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3, isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3, sortBy: 3, indexBy: 3}; // Mix in each Underscore method as a proxy to `Collection#models`. addUnderscoreMethods(Collection, collectionMethods, 'models'); // Backbone.View // ------------- // Backbone Views are almost more convention than they are actual code. A View // is simply a JavaScript object that represents a logical chunk of UI in the // DOM. This might be a single item, an entire list, a sidebar or panel, or // even the surrounding frame which wraps your whole app. Defining a chunk of // UI as a **View** allows you to define your DOM events declaratively, without // having to worry about render order ... and makes it easy for the view to // react to specific changes in the state of your models. // Creating a Backbone.View creates its initial element outside of the DOM, // if an existing element is not provided... // // 視圖構造函數 var View = Backbone.View = function(options) { // 生成惟一標識 this.cid = _.uniqueId('view'); // 綁定實例屬性 _.extend(this, _.pick(options, viewOptions)); // 建立根節點 this._ensureElement(); this.initialize.apply(this, arguments); }; // Cached regex to split keys for `delegate`. var delegateEventSplitter = /^(\S+)\s*(.*)$/; // List of view options to be set as properties. var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; // Set up all inheritable **Backbone.View** properties and methods. _.extend(View.prototype, Events, { // The default `tagName` of a View's element is `"div"`. tagName: 'div', // jQuery delegate for element lookup, scoped to DOM elements within the // current view. This should be preferred to global lookups where possible. // 查詢本視圖做用域中的元素 $: function(selector) { return this.$el.find(selector); }, // Initialize is an empty function by default. Override it with your own // initialization logic. initialize: function(){}, // **render** is the core function that your view should override, in order // to populate its element (`this.el`), with the appropriate HTML. The // convention is for **render** to always return `this`. render: function() { return this; }, // Remove this view by taking the element out of the DOM, and removing any // applicable Backbone.Events listeners. // 移除根節點,銷燬全部監聽事件。 remove: function() { this._removeElement(); this.stopListening(); return this; }, // Remove this view's element from the document and all event listeners // attached to it. Exposed for subclasses using an alternative DOM // manipulation API. // 私有方法,移除根節點。 _removeElement: function() { this.$el.remove(); }, // Change the view's element (`this.el` property) and re-delegate the // view's events on the new element. // 設置根節點元素。包括解綁以前節點委託事件,更換根節點,從新委託事件。 setElement: function(element) { this.undelegateEvents(); this._setElement(element); this.delegateEvents(); return this; }, // Creates the `this.el` and `this.$el` references for this view using the // given `el`. `el` can be a CSS selector or an HTML string, a jQuery // context or an element. Subclasses can override this to utilize an // alternative DOM manipulation API and are only required to set the // `this.el` property. // 私有方法,設置根節點。 // 參數 el 能夠是一個 DocumentElement,或者是一個 jQuery 實例。 _setElement: function(el) { this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); this.el = this.$el[0]; }, // 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.events 做爲委託事件。 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; }, // Add a single event listener to the view's element (or a child element // using `selector`). This only works for delegate-able events: not `focus`, // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. // 委託事件給視圖根節點 delegate: function(eventName, selector, listener) { this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); return this; }, // Clears all callbacks previously bound to the view by `delegateEvents`. // You usually don't need to use this, but may wish to if you have multiple // Backbone views attached to the same DOM element. // 清空根節點全部委託事件 undelegateEvents: function() { if (this.$el) this.$el.off('.delegateEvents' + this.cid); return this; }, // A finer-grained `undelegateEvents` for removing a single delegated event. // `selector` and `listener` are both optional. // 利用 jQuery 清除委託事件 undelegate: function(eventName, selector, listener) { this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener); return this; }, // Produces a DOM element to be assigned to your view. Exposed for // subclasses using an alternative DOM manipulation API. // 建立 DOM 元素(做爲根節點使用) _createElement: function(tagName) { return document.createElement(tagName); }, // 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 = _.extend({}, _.result(this, 'attributes')); // 添加根節點 ID if (this.id) attrs.id = _.result(this, 'id'); // 添加根節點類 if (this.className) attrs['class'] = _.result(this, 'className'); // 建立視圖根節點 this.setElement(this._createElement(_.result(this, 'tagName'))); // 設置根節點 CSS 屬性 this._setAttributes(attrs); } else { // 使用給定的根節點(Element 或 jQuery 實例)建立視圖根節點。 this.setElement(_.result(this, 'el')); } }, // Set attributes from a hash on this view's element. Exposed for // subclasses using an alternative DOM manipulation API. // 設置根節點 CSS 屬性 _setAttributes: function(attributes) { this.$el.attr(attributes); } }); // Backbone.sync // ------------- // Override this function to change the manner in which Backbone persists // models to the server. You will be passed the type of request, and the // model in question. By default, makes a RESTful Ajax request // to the model's `url()`. Some possible customizations could be: // // * Use `setTimeout` to batch rapid-fire updates into a single request. // * Send up the models as XML instead of JSON. // * Persist models via WebSockets instead of Ajax. // // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests // as `POST`, with a `_method` parameter containing the true HTTP method, // as well as all requests with the body as `application/x-www-form-urlencoded` // instead of `application/json` with the model in a param named `model`. // Useful when interfacing with server-side languages like **PHP** that make // it difficult to read the body of `PUT` requests. // // 若是啓用 `Backbone.emulatedHTTP` ,那麼 Backbone 會將 `PUT` 和 `DELETE` 請求改成 `POST` 請求, // 同時增長一個 `_method` 參數用以記錄本來的請求方法。 Backbone.sync = function(method, model, options) { // sync 函數參數 method 取值範圍爲:create, read, update, delete, patch; // 分別映射到 HTTP 請求方法:POST, GET, PUT, DELETE, PATCH // 這裏是將 sync 的 method 轉換爲 HTTP 請求方法名。 var type = methodMap[method]; // Default options, unless specified. _.defaults(options || (options = {}), { emulateHTTP: Backbone.emulateHTTP, emulateJSON: Backbone.emulateJSON }); // 默認請求 JSON 數據。 // 局部變量 params 表示最後 ajax 請求參數 var params = {type: type, dataType: 'json'}; // 檢查是否輸入 URL 或者 model 是否自帶 URL if (!options.url) { params.url = _.result(model, 'url') || urlError(); } // Ensure that we have the appropriate request data. // 若是 options 未給定 data 字段(即 model.fetch(options) 中的 options) // 而且同步的方法是寫操做,那麼默認的 xhr 請求中 contentType 應爲 json。 // 提交的 data 優先從 options.attrs 讀取,其次讀取 model.toJSON()。 if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { params.contentType = 'application/json'; params.data = JSON.stringify(options.attrs || model.toJSON(options)); } // 若是設置了 Backbone.emulateJSON 爲真,則使用 application/x-www-form-urlencoded 格式提交數據。 // 注意: // 這裏並非將 data 直接編碼成 HTML-form 格式,而是將整個 data 封裝在 model 字段中提交。 // 若是不這樣作,當 model 爲 collection 時,實際的 data 是一個數組,不適宜做爲 form 提交。 if (options.emulateJSON) { params.contentType = 'application/x-www-form-urlencoded'; params.data = params.data ? {model: params.data} : {}; } // For older servers, emulate HTTP by mimicking the HTTP method with `_method` // And an `X-HTTP-Method-Override` header. // 若是設置 Backbone.emulateHTTP 爲真,且 sync 爲寫操做, // 則統一使用 POST 方法請求,而且將原始請求方法保存在 data._method 字段中。 // 同時增長 xhr 請求頭 X-HTTP-Method-Override。 if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { params.type = 'POST'; if (options.emulateJSON) params.data._method = type; var beforeSend = options.beforeSend; options.beforeSend = function(xhr) { xhr.setRequestHeader('X-HTTP-Method-Override', type); if (beforeSend) return beforeSend.apply(this, arguments); }; } // Don't process data on a non-GET request. // jQeury 的 ajax 方法,若是提交的 data 爲非字符串對象,會被默認轉換爲 query string。 // 以匹配默認的 application/x-www-form-urlencode 類型文檔。 // 所以對於非 GET 且未要求 emulateJSON 的請求,設置 processData 爲否以阻止 jQuery 這一默認行爲。 if (params.type !== 'GET' && !options.emulateJSON) { params.processData = false; } // 從新封裝 options 中的 error,將 textStatus 和 errorThrown 記錄到 options 中。 var error = options.error; options.error = function(xhr, textStatus, errorThrown) { options.textStatus = textStatus; options.errorThrown = errorThrown; if (error) error.call(options.context, xhr, textStatus, errorThrown); }; // 使用 Backbone.ajax 發起 xhr 請求,而且將 xhr 保存在 options 中。 var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); // 發起請求後當即出發 request 事件,告知第三方已發起了一次 xhr 請求。 model.trigger('request', model, xhr, options); return xhr; }; // 默認的 Backbone.sync 中 method 與 http 請求的映射關係。 // 爲何要自定義一套 Backbone.sync 的方法名而不直接使用 HTTP 請求方法名? // 由於這樣能夠將 Backbone 的同步操做與 HTTP 請求分離開, // 由於你也能夠經過其餘渠道來實現數據同步,例如經過改寫 sync 方法來與 local storage 同步數據。 var methodMap = { 'create': 'POST', 'update': 'PUT', 'patch': 'PATCH', 'delete': 'DELETE', 'read': 'GET' }; // Set the default implementation of `Backbone.ajax` to proxy through to `$`. // Override this if you'd like to use a different library. // 默認使用 jQuery.ajax 方法實現數據同步,若是使用其餘庫,能夠改寫此同步方法。 Backbone.ajax = function() { return Backbone.$.ajax.apply(Backbone.$, arguments); }; // Backbone.Router - 路由 // ------------------------- // Routers map faux-URLs to actions, and fire events when routes are // matched. Creating a new one sets its `routes` hash, if not set statically. // // Router 構造函數 var Router = Backbone.Router = function(options) { options || (options = {}); if (options.routes) this.routes = options.routes; this._bindRoutes(); this.initialize.apply(this, arguments); }; // Cached regular expressions for matching named param parts and splatted // parts of route strings. var optionalParam = /\((.*?)\)/g; var namedParam = /(\(\?)?:\w+/g; var splatParam = /\*\w+/g; var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; // Set up all inheritable **Backbone.Router** properties and methods. _.extend(Router.prototype, Events, { // Initialize is an empty function by default. Override it with your own // initialization logic. initialize: function(){}, // Manually bind a single named route to a callback. For example: // // this.route('search/:query/p:num', 'search', function(query, num) { // ... // }); // // 手動添加路由 // @param route: 字符串或正則表達式,表示路由路徑。 // @param name: 路由名稱,表示路由器處理路由的方法(this[name]),或者 name 就是響應函數(至關於 callback)。 // @param callback: 若是沒有給定 callback,則使用 this[name],不然使用 callback 做爲路由響應函數。 route: function(route, name, callback) { // 若是 route 不是正則表達式,則將其轉換爲正則表達式。 if (!_.isRegExp(route)) route = this._routeToRegExp(route); if (_.isFunction(name)) { callback = name; name = ''; } if (!callback) callback = this[name]; var router = this; // 在 Backbone.history 中添加路由(正則表達式) Backbone.history.route(route, function(fragment) { var args = router._extractParameters(route, fragment); // 若是 router.execute 方法返回 false,則不觸發任何事件。 // 默認 router.execute 方法返回值固定爲 void 0,所以必定會觸發事件。 // 若是要阻止觸發事件,只能是重寫 router.execute 方法。 if (router.execute(callback, args, name) !== false) { // 是的,若是 route 第二個參數爲函數,那麼 name 就是空字符串。 // 所以觸發的事件是 'route:'。 // router 觸發了兩個看似相同的事件,一個是 `route:name`,另外一個是 `router`。 router.trigger.apply(router, ['route:' + name].concat(args)); router.trigger('route', name, args); Backbone.history.trigger('route', router, name, args); } }); return this; }, // Execute a route handler with the provided parameters. This is an // excellent place to do pre-route setup or post-route cleanup. // 執行路由回調函數。 execute: function(callback, args, name) { if (callback) callback.apply(this, args); }, // Simple proxy to `Backbone.history` to save a fragment into the history. navigate: function(fragment, options) { Backbone.history.navigate(fragment, options); return this; }, // Bind all defined routes to `Backbone.history`. We have to reverse the // order of the routes here to support behavior where the most general // routes can be defined at the bottom of the route map. // 將全部路由綁定到 `Backbone.history`。 _bindRoutes: function() { // 若是未定義路由,則終止綁定操做。 if (!this.routes) return; // 對 this.routes 求值。 this.routes = _.result(this, 'routes'); var route, routes = _.keys(this.routes); // 遍歷 this.routes,逐一添加路由 while ((route = routes.pop()) != null) { this.route(route, this.routes[route]); } }, // Convert a route string into a regular expression, suitable for matching // against the current location hash. _routeToRegExp: function(route) { route = route.replace(escapeRegExp, '\\$&') .replace(optionalParam, '(?:$1)?') .replace(namedParam, function(match, optional) { return optional ? match : '([^/?]+)'; }) .replace(splatParam, '([^?]*?)'); return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); }, // Given a route, and a URL fragment that it matches, return the array of // extracted decoded parameters. Empty or unmatched parameters will be // treated as `null` to normalize cross-browser behavior. // // 從路由路徑中提取參數。 // @param route: 路由正則表達式 // @param fragment: 被 Backbone.History 確認匹配的 URL 路徑。 _extractParameters: function(route, fragment) { var params = route.exec(fragment).slice(1); return _.map(params, function(param, i) { // Don't decode the search params. if (i === params.length - 1) return param || null; return param ? decodeURIComponent(param) : null; }); } }); // Backbone.History // ---------------- // Handles cross-browser history management, based on either // [pushState](http://diveintohtml5.info/history.html) and real URLs, or // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) // and URL fragments. If the browser supports neither (old IE, natch), // falls back to polling. // // 使用 HTML5 History API 或者 onhashchange 事件實現歷史記錄操做。 var History = Backbone.History = function() { this.handlers = []; this.checkUrl = _.bind(this.checkUrl, this); // Ensure that `History` can be used outside of the browser. if (typeof window !== 'undefined') { this.location = window.location; this.history = window.history; } }; // Cached regex for stripping a leading hash/slash and trailing space. // 正則表達式:用以刪除字符串頭部的 `#` 或 `/` 字符,以及尾部的空白。 // 例如:'/abc/ '.replace(routeStripper, '') 獲得 'abc/' var routeStripper = /^[#\/]|\s+$/g; // Cached regex for stripping leading and trailing slashes. // 正則表達式:刪除字符串頭尾的 `/` 字符(確保字符串不以 `/` 開頭或結尾) var rootStripper = /^\/+|\/+$/g; // Cached regex for stripping urls of hash. // 正則表達式:刪除字符串中 '#' 字符(包含井字符)後全部字符。 var pathStripper = /#.*$/; // Has the history handling already been started? History.started = false; // Set up all inheritable **Backbone.History** properties and methods. _.extend(History.prototype, Events, { // The default interval to poll for hash changes, if necessary, is // twenty times a second. interval: 50, // Are we at the app root? atRoot: function() { var path = this.location.pathname.replace(/[^\/]$/, '$&/'); return path === this.root && !this.getSearch(); }, // Does the pathname match the root? matchRoot: function() { var path = this.decodeFragment(this.location.pathname); var root = path.slice(0, this.root.length - 1) + '/'; return root === this.root; }, // Unicode characters in `location.pathname` are percent encoded so they're // decoded for comparison. `%25` should not be decoded since it may be part // of an encoded parameter. // 將 fragment 從百分號編碼解碼成 UNICODE,但不解碼 `%25`,由於它有多是被編碼的參數。 decodeFragment: function(fragment) { return decodeURI(fragment.replace(/%25/g, '%2525')); }, // In IE6, the hash fragment and search params are incorrect if the // fragment contains `?`. getSearch: function() { var match = this.location.href.replace(/#.*/, '').match(/\?.+/); return match ? match[0] : ''; }, // Gets the true hash value. Cannot use location.hash directly due to bug // in Firefox where location.hash will always be decoded. getHash: function(window) { var match = (window || this).location.href.match(/#(.*)$/); return match ? match[1] : ''; }, // Get the pathname and search params, without the root. getPath: function() { var path = this.decodeFragment( this.location.pathname + this.getSearch() ).slice(this.root.length - 1); return path.charAt(0) === '/' ? path.slice(1) : path; }, // Get the cross-browser normalized URL fragment from the path or hash. getFragment: function(fragment) { if (fragment == null) { if (this._usePushState || !this._wantsHashChange) { fragment = this.getPath(); } else { fragment = this.getHash(); } } return fragment.replace(routeStripper, ''); }, // Start the hash change handling, returning `true` if the current URL matches // an existing route, and `false` otherwise. // 啓動 History 路由,若是當前 URL 匹配到了某條路由,返回 true,不然返回 false。 start: function(options) { // History 是個單例應用,不容許重複啓動。 if (History.started) throw new Error('Backbone.history has already been started'); History.started = true; // Figure out the initial configuration. Do we need an iframe? // Is pushState desired ... is it available? this.options = _.extend({root: '/'}, this.options, options); this.root = this.options.root; this._wantsHashChange = this.options.hashChange !== false; this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7); this._useHashChange = this._wantsHashChange && this._hasHashChange; this._wantsPushState = !!this.options.pushState; this._hasPushState = !!(this.history && this.history.pushState); this._usePushState = this._wantsPushState && this._hasPushState; this.fragment = this.getFragment(); // Normalize root to always include a leading and trailing slash. this.root = ('/' + this.root + '/').replace(rootStripper, '/'); // Transition from hashChange to pushState or vice versa if both are // requested. if (this._wantsHashChange && this._wantsPushState) { // If we've started off with a route from a `pushState`-enabled // browser, but we're currently in a browser that doesn't support it... if (!this._hasPushState && !this.atRoot()) { var root = this.root.slice(0, -1) || '/'; this.location.replace(root + '#' + this.getPath()); // Return immediately as browser will do redirect to new url return true; // Or if we've started out with a hash-based route, but we're currently // in a browser where it could be `pushState`-based instead... } else if (this._hasPushState && this.atRoot()) { this.navigate(this.getHash(), {replace: true}); } } // Proxy an iframe to handle location events if the browser doesn't // support the `hashchange` event, HTML5 history, or the user wants // `hashChange` but not `pushState`. if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) { this.iframe = document.createElement('iframe'); this.iframe.src = 'javascript:0'; this.iframe.style.display = 'none'; this.iframe.tabIndex = -1; var body = document.body; // Using `appendChild` will throw on IE < 9 if the document is not ready. var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow; iWindow.document.open(); iWindow.document.close(); iWindow.location.hash = '#' + this.fragment; } // Add a cross-platform `addEventListener` shim for older browsers. var addEventListener = window.addEventListener || function (eventName, listener) { return attachEvent('on' + eventName, listener); }; // Depending on whether we're using pushState or hashes, and whether // 'onhashchange' is supported, determine how we check the URL state. if (this._usePushState) { addEventListener('popstate', this.checkUrl, false); } else if (this._useHashChange && !this.iframe) { addEventListener('hashchange', this.checkUrl, false); } else if (this._wantsHashChange) { this._checkUrlInterval = setInterval(this.checkUrl, this.interval); } if (!this.options.silent) return this.loadUrl(); }, // Disable Backbone.history, perhaps temporarily. Not useful in a real app, // but possibly useful for unit testing Routers. stop: function() { // Add a cross-platform `removeEventListener` shim for older browsers. var removeEventListener = window.removeEventListener || function (eventName, listener) { return detachEvent('on' + eventName, listener); }; // Remove window listeners. if (this._usePushState) { removeEventListener('popstate', this.checkUrl, false); } else if (this._useHashChange && !this.iframe) { removeEventListener('hashchange', this.checkUrl, false); } // Clean up the iframe if necessary. if (this.iframe) { document.body.removeChild(this.iframe); this.iframe = null; } // Some environments will throw when clearing an undefined interval. if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); History.started = false; }, // Add a route to be tested when the fragment changes. Routes added later // may override previous routes. route: function(route, callback) { this.handlers.unshift({route: route, callback: callback}); }, // Checks the current URL to see if it has changed, and if it has, // calls `loadUrl`, normalizing across the hidden iframe. checkUrl: function(e) { var current = this.getFragment(); // If the user pressed the back button, the iframe's hash will have // changed and we should use that for comparison. if (current === this.fragment && this.iframe) { current = this.getHash(this.iframe.contentWindow); } if (current === this.fragment) return false; if (this.iframe) this.navigate(current); this.loadUrl(); }, // Attempt to load the current URL fragment. If a route succeeds with a // match, returns `true`. If no defined routes matches the fragment, // returns `false`. loadUrl: function(fragment) { // If the root doesn't match, no routes can match either. if (!this.matchRoot()) return false; fragment = this.fragment = this.getFragment(fragment); return _.some(this.handlers, function(handler) { if (handler.route.test(fragment)) { handler.callback(fragment); return true; } }); }, // Save a fragment into the hash history, or replace the URL state if the // 'replace' option is passed. You are responsible for properly URL-encoding // the fragment in advance. // // The options object can contain `trigger: true` if you wish to have the // route callback be fired (not usually desirable), or `replace: true`, if // you wish to modify the current URL without adding an entry to the history. navigate: function(fragment, options) { if (!History.started) return false; if (!options || options === true) options = {trigger: !!options}; // Normalize the fragment. fragment = this.getFragment(fragment || ''); // Don't include a trailing slash on the root. var root = this.root; if (fragment === '' || fragment.charAt(0) === '?') { root = root.slice(0, -1) || '/'; } var url = root + fragment; // Strip the hash and decode for matching. fragment = this.decodeFragment(fragment.replace(pathStripper, '')); if (this.fragment === fragment) return; this.fragment = fragment; // If pushState is available, we use it to set the fragment as a real URL. if (this._usePushState) { this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); // If hash changes haven't been explicitly disabled, update the hash // fragment to store history. } else if (this._wantsHashChange) { this._updateHash(this.location, fragment, options.replace); if (this.iframe && (fragment !== this.getHash(this.iframe.contentWindow))) { var iWindow = this.iframe.contentWindow; // Opening and closing the iframe tricks IE7 and earlier to push a // history entry on hash-tag change. When replace is true, we don't // want this. if (!options.replace) { iWindow.document.open(); iWindow.document.close(); } this._updateHash(iWindow.location, fragment, options.replace); } // If you've told us that you explicitly don't want fallback hashchange- // based history, then `navigate` becomes a page refresh. } else { return this.location.assign(url); } if (options.trigger) return this.loadUrl(fragment); }, // Update the hash location, either replacing the current entry, or adding // a new one to the browser history. _updateHash: function(location, fragment, replace) { if (replace) { var href = location.href.replace(/(javascript:|#).*$/, ''); location.replace(href + '#' + fragment); } else { // Some browsers require that `hash` contains a leading #. location.hash = '#' + fragment; } } }); // Create the default Backbone.history. Backbone.history = new History; // Helpers // ------- // Helper function to correctly set up the prototype chain for subclasses. // Similar to `goog.inherits`, but uses a hash of prototype properties and // class properties to be extended. // `extend` 函數經過設置子類的原型鏈實現繼承機制,它能夠同時擴展父類的原型 // 屬性和類屬性。 var extend = function(protoProps, staticProps) { var parent = this; // 上下文應指向父類 var child; // 子類(構造函數) // 當傳入原型對象包含 `constructor` 屬性,則直接做爲子類的構造函數。 // 不然新建一個構造函數,並在構造函數中調用父類構造函數。 if (protoProps && _.has(protoProps, 'constructor')) { child = protoProps.constructor; } else { child = function(){ return parent.apply(this, arguments); }; } // 將父類靜態屬性和新傳入的靜態屬性擴展到子類上。 _.extend(child, parent, staticProps); // 設置中間人(或者代理構造函數),將中間人 constructor 屬性設置爲子類構造函數。 // 將子類的 prototype 設置爲中間人實例,從而使得子類處於中間人原型鏈上。 // 避免將子類 prototype 直接指向中間人的 prototype,可使得對父類 prototype 的修改, // 直接做用到子類上,但對子類 prototype 的修改,會被父類實例隔絕,從而避免做用到父類身上。 // // 使用中間人鏈接 child 和 parent,不將 child 的 prototype 直接指向 parent 的 prototype。 // 緣由在於子類 prototype 應指向父類實例,從而避免原型鏈上的逆向做用。 // 使用中間人,將中間人的 prototype 指向 parent 的 prototype, // 能夠保證明現繼承同時避免調用 parent 的構造函數,從而帶來反作用。 var Surrogate = function(){ this.constructor = child; }; Surrogate.prototype = parent.prototype; child.prototype = new Surrogate; // 擴展子類的原型(實例方法) if (protoProps) _.extend(child.prototype, protoProps); // 添加 __super__ 屬性指向父類的原型,以便在子類中能夠調用父類原型。 child.__super__ = parent.prototype; return child; }; // Set up inheritance for the model, collection, router, view and history. Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; // Throw an error when a URL is needed, and none is supplied. // 異常:URL 不存在 var urlError = function() { throw new Error('A "url" property or function must be specified'); }; // Wrap an optional error callback with a fallback error event. // 封裝 error 回調(在 model.fetch 方法中,ajax 的 error 回調) // 保證不管是否存在 options.error 回調,都會觸發 model 的 error 事件。 var wrapError = function(model, options) { var error = options.error; options.error = function(resp) { if (error) error.call(options.context, model, resp, options); model.trigger('error', model, resp, options); }; }; return Backbone; }));