前言
昨天我們一起學習了backbone的model,我個人對backbone的熟悉程度提高了,但是也發現一個嚴重的問題!!!
我平時壓根沒有用到model這塊的東西,事實上我只用到了view,所以昨天學習的效果其實不佳,比起上次對underscore的熟悉,對zepto的熟悉,甚至對fastclick的熟悉
學習效率打了折扣,而且一些地方不明不白,所以,我今天決定將速度放慢,我們學習collection時候先做小例子,爭取覆蓋關鍵點,然后再從源碼學習,於是開始吧
集合-Collection
實例解析
首先回到我們熟悉的官方例子,其中關於集合的敘述不多,官方的例子流程如下:
① 定義模型Model
這個model很簡單,基本沒有干什么有意義的事情,驗證的事情都沒有干,說到這里有一點需要注意:
我們在實際項目中,無論是使用backbone的model或者自己實現了model
都一定要提供validate驗證機制,或者驗證方法,這個可以讓我們的程序更加穩健!
舉個例子來說,我們的程序中model會讀取來自服務器端的數據,正常情況下是這樣的:
{ data1: [{name: '葉小釵', ......},], data2: ...... }
但是如果有一天我們的服務器出錯了,他會返回錯誤的數據,或者空數據,而前端的代碼沒有做容錯處理的話,這個數據就會被寫入localstorage
{
data1: {},
data2: {}
}
這個json對象當然不為空,因為我們model的機制是為空時候會拉取數據,不為空則直接讀取localstorage數據,但是事實上,在業務邏輯上這個數據是為空的!!!
所以,我們的程序表現上就是列表為空,讀不到數據了,這可以引發很不好的問題
但是如果我們這個model有一個validate方法用於驗證model的正確性的話,就可以避免這個問題了!
以我們官方的例子來說,他這個Todo的model事實上應該驗證title為空的情況,但他沒有這么做,他將數據的驗證放到了view里面
1 createOnEnter: function (e) { 2 if (e.keyCode != 13) return; 3 if (!this.input.val()) return; 4 5 Todos.create({ title: this.input.val() }); 6 this.input.val(''); 7 },
但是如果我們導入model數據的方式不止一種,或者要批量導入時候,view要面臨的問題就相對復雜一點了,總而言之Todo模型應該具有validate驗證函數
validate函數不返回數據就是驗證成功,否則失敗是不會執行set方法的,所以,我這里將view中的驗證去掉,將驗證寫入model,我們先走一次流程:
1 AppView會觸發createOnEnter事件,本來這里有一個為空驗證,被我破壞掉了
1 createOnEnter: function (e) { 2 if (e.keyCode != 13) return; 3 // if (this.input.val() == '') return; 4 5 Todos.create({ title: this.input.val() }); 6 this.input.val(''); 7 },
2 執行集合的create方法
1 // 向集合中添加並創建一個模型, 同時將該模型保存到服務器 2 // 如果是通過數據對象來創建模型, 需要在集合中聲明model屬性對應的模型類 3 // 如果在options中聲明了wait屬性, 則會在服務器創建成功后再將模型添加到集合, 否則先將模型添加到集合, 再保存到服務器(無論保存是否成功) 4 create: function (model, options) { 5 var coll = this; 6 // 定義options對象 7 options = options ? _.clone(options) : {}; 8 // 通過_prepareModel獲取模型類的實例 9 model = this._prepareModel(model, options); 10 // 模型創建失敗 11 if (!model) 12 return false; 13 // 如果沒有聲明wait屬性, 則通過add方法將模型添加到集合中 14 if (!options.wait) 15 coll.add(model, options); 16 // success存儲保存到服務器成功之后的自定義回調函數(通過options.success聲明) 17 var success = options.success; 18 // 監聽模型數據保存成功后的回調函數 19 options.success = function (nextModel, resp, xhr) { 20 // 如果聲明了wait屬性, 則在只有在服務器保存成功后才會將模型添加到集合中 21 if (options.wait) 22 coll.add(nextModel, options); 23 // 如果聲明了自定義成功回調, 則執行自定義函數, 否則將默認觸發模型的sync事件 24 if (success) { 25 success(nextModel, resp); 26 } else { 27 nextModel.trigger('sync', model, resp, options); 28 } 29 }; 30 // 調用模型的save方法, 將模型數據保存到服務器 31 model.save(null, options); 32 return model; 33 },
3 這里的model需要通過_prepareModel進行處理
1 // 將模型添加到集合中之前的一些准備工作 2 // 包括將數據實例化為一個模型對象, 和將集合引用到模型的collection屬性 3 _prepareModel: function (model, options) { 4 options || (options = {}); 5 // 檢查model是否是一個模型對象(即Model類的實例) 6 if (!(model instanceof Model)) { 7 // 傳入的model是模型數據對象, 而並非模型對象 8 // 將數據作為參數傳遞給Model, 以創建一個新的模型對象 9 var attrs = model; 10 // 設置模型引用的集合 11 options.collection = this; 12 // 將數據轉化為模型 13 model = new this.model(attrs, options); 14 // 對模型中的數據進行驗證 15 if (!model._validate(model.attributes, options)) 16 model = false; 17 } else if (!model.collection) { 18 // 如果傳入的是一個模型對象但沒有建立與集合的引用, 則設置模型的collection屬性為當前集合 19 model.collection = this; 20 } 21 return model; 22 },
他在13行進行了model實例化,但是並沒有執行我們的validate驗證,接下來便是執行了一次validate驗證,如果驗證不通過就完蛋
這里比較煩的是,他這里沒有走我們的驗證流程,因為在第一個判斷就直接跳出來了,而save時候驗證了數據有效性,所以數據沒有寫往localstorage但是頁面上卻多了一條記錄
這里的處理,我其實是認為有問題的,這里未驗證其實是因為初始化時候不需要驗證,但是我們傳一個參數就會發生驗證
Todos.create({ title: this.input.val() }, {validate: true});
在這里,我們扯得有點遠了,稍微回顧了下昨天學習的model的知識,現在繼續向后面走
② 定義collection
有了Todo模型后,於是便定義了一個集合,並且對其進行了實例化
這個例子作為demo沒什么問題,但是一般項目中,list不可能像這樣全局化,多是與view進行關聯,view之間以localstorage進行數據通信
所以跳往 b view時候數據早在a view時候已經准備好,因為我也沒有使用backbone的collection進行項目開發,這里不太能看清意圖,但是在view里面進行實例化比較靠譜
比如在APPView的initialize中進行實例化,而構造函數以require的方式進行引入
backbone的集合本身方法多源於underscore的集合方法:
1 var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 2 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 3 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 4 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 5 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', 6 'isEmpty', 'chain']; 7 8 // Mix in each Underscore method as a proxy to `Collection#models`. 9 _.each(methods, function (method) { 10 Collection.prototype[method] = function () { 11 var args = slice.call(arguments); 12 args.unshift(this.models); 13 return _[method].apply(_, args); 14 }; 15 });
這里依然有幾點需要注意:
1 因為這里未與服務器發生交互,用於存儲數據的就是localstorage
localStorage: new Backbone.LocalStorage("todos-backbone"),
持久層我們一般會自己實現,與服務器的交互也不會使用backbone本身的,所以model與持久層通信這塊可以選擇放棄
2 這里TodoList提供了幾個方法多用於篩選數據,本身底層是調用underscore的方法篩選/排序自己內部models
但是model里面居然使用了集合中的nextOrder方法獲取順序標識,這里不知道各位怎么看,我反正覺得怪怪的.....
1 return { 2 title: "empty todo...", 3 order: Todos.nextOrder(), 4 done: false 5 };
model里面的這個order我個人覺得十分不好,有可能我這個模型還得用於其它集合,而其它集合未必是Todos,但是這里寫死了
而且其它集合未必具有nextOrder這個方法,所以官方這個例子真的就只是一個demo啊......
list方面本身沒什么難度,根據我們這兩天的學習可以做一個總結:
Backbone Model
用於封裝數據對象,並且提供數據改變時候的change事件以及數據正確性驗證的validate方法
總而言之,這個model就是用於數據操作,並且數據改變時候可以通知到view
Backbone Collection
用於封裝Model對象,其中提供了很多處理model的方法,比如排序分組什么的
當然,這個只是第一階段的認識,更多的了解根據學習的深入會逐步展開,而且我感覺要深入學習Backbone還是得好好的用一用
③ 綁定事件
從程序可以看出Todos(實例化的TodoList)只用於了AppView,而這里的AppView有點擔當了控制器的意思,
在實例化AppView時,便為Todos注冊了三大事件,我們Todolist繼承了Events對象,各位不要忘了哦
1 this.listenTo(Todos, 'add', this.addOne); 2 this.listenTo(Todos, 'reset', this.addAll); 3 this.listenTo(Todos, 'all', this.render);
初始化結束后會執行fetch函數獲得數據,數據獲取結束便又會調用model set方法裝入數據,並且觸發model的change事件,而導致TodoView的render觸發
然后AppView獲取TodoView的dom結構后將其dom結構展示到頁面即可
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'destroy', this.remove);
於是我們實例解析便基本結束了,接下來看看源碼相關
構造函數
backbone中的集合時模型的有序組合,我們可以在集合上綁定change事件,從而當集合中的模型發生變化時獲得通知
集合也可以注冊事件,從服務器端得到更新,集合中的模型觸發的任何事件都可以在集合上直接觸發,我們可以監聽集合中模型的變化
首先依然來看看他的初始化的構造函數:
1 var Collection = Backbone.Collection = function (models, options) { 2 options || (options = {}); 3 if (options.url) this.url = options.url; 4 if (options.model) this.model = options.model; 5 if (options.comparator !== void 0) this.comparator = options.comparator; 6 this._reset(); 7 this.initialize.apply(this, arguments); 8 if (models) this.reset(models, _.extend({ silent: true }, options)); 9 };
前幾行沒什么好說的,其中實例化時會重置集合內部的狀態,第一次定義即為初始化,第二次為重置
1 _reset: function () { 2 this.length = 0; 3 this.models = []; 4 this._byId = {}; 5 },
讓后調用初始化方法initialize,如果這里設置了多個models的話,會執行reset方法,如果指定了models數據, 則調用reset方法將數據添加到集合中
首次調用時設置了silent參數, 因此不會觸發"reset"事件
1 reset: function (models, options) { 2 options || (options = {}); 3 for (var i = 0, l = this.models.length; i < l; i++) { 4 this._removeReference(this.models[i]); 5 } 6 options.previousModels = this.models; 7 this._reset(); 8 this.add(models, _.extend({ silent: true }, options)); 9 if (!options.silent) this.trigger('reset', this, options); 10 return this; 11 },
reset用於替換集合中所有模型數據,該操作將刪除集合中當前的數據和狀態,重新設置models,models一般為二維數據對象
這里會刪除原model與集合的映射關系,但是並沒有刪除model本身
_removeReference: function (model) { if (this === model.collection) delete model.collection; model.off('all', this._onModelEvent, this); },
其實從官網的代碼來看,構造函數里面的東西基本沒有用到,官網的代碼的集合充其量就是一個容器,所以要學習這塊還需要更好的例子
結語
這篇博客暫時到這里,我發現沒有好的例子學習效率確實不高,下來先寫一個例子,再根據例子學習吧